npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hasna/projects

v1.0.4

Published

High-level project management CLI, MCP server, SDK, and AI SDK agent loop for coding agents

Readme

@hasna/projects

High-level project management CLI, MCP server, and SDK for AI coding agents. A project can be any repository, app, docs folder, scaffold, experiment, or remote-intended project in any folder. Projects tracks roots, recipes, agents, tmux profiles, immutable project events, and prompt-driven AI agent runs.

Project Resources

Install

bun install -g @hasna/projects

CLI

The app and binary are named projects, and the public command surface is project-first.

# Prompt mode through AI SDK + OpenRouter
projects --dry-run --json "Plan a new open source project named Log Tools in /tmp/log-tools with tmux"
projects --yes "Create a docs project in /home/me/docs/new-docs and write a marker"
projects --model openai/gpt-4o-mini --max-steps 8 "Import this folder as a project"

# Roots
projects roots add --name "Open Source" --path /home/me/opensource --kind open-source --path-template "{slug}"
projects roots list --json
projects roots match --path /home/me/opensource/my-app --json
projects roots update open-source --github-org hasna --visibility public

# Recipes
projects recipes add --name "TypeScript Library" --kind open-source --tags typescript,library
projects recipes list
projects recipes built-ins
projects recipes seed-defaults --json

# Agents
projects agents add --name "Codex" --kind ai --provider openrouter --model openai/gpt-4o-mini
projects agents list

# Tmux profiles
projects tmux-profiles add --name "Dev" --slug dev --session-template "{slug}-dev" \
  --windows-json '[{"name":"editor"},{"name":"server","command":"bun run dev"}]'
projects tmux-profiles apply dev my-project --dry-run --json

# Projects
projects create --name "My App" --path /path/to/my-app --stage active --priority high \
  --owner hasna --launch-profile dev --start-agent claude --start-command "claude --resume" \
  --start-session-policy error-if-running \
  --start-windows-json '[{"name":"server","command":"bun run dev"}]' \
  --todos-project-id todo_123 --brief-id brief_123 --mkdir --git-init --marker --json
projects create --name "Planned App" --path /tmp/planned --mkdir --dry-run --json
projects create --name "Store Work" --kind project --mkdir --marker --json  # defaults to $HASNA_PROJECTS_HOME/workspaces/<id>
projects start                              # from inside a registered repo
projects start notes                  # by slug/id/name/path
projects start --json                      # operational structured output
projects start --render-spec               # validated JSON Render spec
projects start my-app --agent codewith
projects start /path/to/existing --agent claude
projects start my-app --windows-json '[{"name":"editor","command":"code ."},{"name":"server","command":"bun run dev"}]'
projects start /path/to/new-folder --tags family,security --metadata-json '{"domain":"home-security"}' --dry-run --json
projects start my-app --profile dev --agent claude --new
projects start my-app --error-if-running --agent none
projects start --rename-report --agent codewith
projects sessions my-app --unrenamed --json
projects start --bulk my-app docs-site service-api --agent opencode --dry-run --json
projects start --bulk-file ./project-targets.json --agent claude --dry-run --json
projects start --label kind:work-project --dry-run --json
projects status my-app --profile dev --json
projects store inspect my-app --json
projects canvases list my-app --ensure-default --render-spec
projects canvases create my-app --name "Research Board" --nodes-json '[{"id":"note","position":{"x":0,"y":0},"data":{"title":"Note"}}]'
projects canvases upsert my-app --slug planning-board --nodes-json '[{"id":"summary","type":"projectPanel","position":{"x":0,"y":0},"data":{"title":"Summary"}}]'
projects canvases compose my-app --spec-json '{"slug":"directory","blocks":[{"id":"summary","title":"Summary"},{"id":"table","title":"Table","columns":["name","role"],"rows":[{"name":"Ada","role":"Lead"}]}],"links":[{"source":"summary","target":"table"}]}'
projects dashboard snapshot my-app --write --json
projects dashboard render my-app --json
projects dashboard validate my-app --json
PROJECTS_DASHBOARD_TOKEN="<token>" projects dashboard serve my-app --host 0.0.0.0 --port 3344
projects reports serve --port 3345
PROJECTS_REPORTS_TOKEN="<token>" projects reports serve --host 0.0.0.0 --port 3345
projects loops link my-app daily-check --name "Daily Check" --role maintenance
projects loops list my-app --json
projects import /path/to/existing --json
projects import-github hasna/example --root open-source --clone --dry-run --json
projects scan-roots --root open-source --repo-prefix project- --clone --json
projects sync-roots --root open-source --repo-prefix project- --tags open-source,project --json
projects import /path/to/root --bulk --dry-run --json
projects list --query app --tags web,ts
projects list --label org:hasnaxyz --json
projects list --limit 50 --verbose
projects list --query app --tags web,ts --json
projects show my-app --json
projects show my-app --verbose
projects events list my-app --limit 10 --verbose
projects get my-app --json
projects update my-app --description "New description" --tags web,ts --priority critical --launch-profile dev
projects tag my-app security cameras
projects untag my-app cameras
projects labels add my-app org:hasnaxyz kind:work-project client:foo
projects labels list my-app
projects labels remove my-app client:foo
projects oss matrix --root /home/me/opensource --prefix open- --json
projects oss matrix --root /home/me/opensource --limit 50 --no-prs --no-tasks
projects store inspect my-app --json
projects store ensure my-app --json
projects store ensure wks_exact_stable_id --dry-run --json  # required target form on the hosted backend
projects store migrate my-app --json          # dry-run plan
projects store migrate my-app --apply --json  # explicit move/update
projects link my-app --github-url https://github.com/hasna/my-app --todos-project-id todo_123 --todos-task-list-id list_123
projects unlink my-app --todos --brief
projects locations add my-app /path/to/another-folder --label docs
projects locations list my-app
projects archive my-app
projects unarchive my-app
projects delete my-app

# Budgets
projects budgets set --project my-app --max-usd 5 --max-total-tokens 100000 --json
projects budgets remaining --project my-app --json
projects --budget-project my-app --run-budget-tokens 2000 --json "Plan the next release"

# Project checks
projects doctor my-app --fix --dry-run --json

# Agent evals
projects agent-eval --json
projects agent-eval --mock --json
projects agent-eval --case create-explicit-path,tmux-apply-existing --fail-on-error

# Agent assist — help coding agents orient, decide, and continue
projects context my-app --for-agent        # one-shot priming bundle
projects next my-app --for-agent           # suggested next actions
projects why my-app --for-agent            # resolution trace + fix tips
projects handoff my-app --for-agent        # cross-agent/machine handoff bundle
projects runs list my-app --for-agent      # prompt-agent run ledger
projects runs show <run-id> my-app         # full run detail + tool-call trace
# All six also emit JSON with -j/--json and are available as MCP tools:
#   projects_context, projects_next, projects_why, projects_handoff,
#   projects_runs_list, projects_runs_show

# Conversations channel linkage — every project has one channel
projects channel my-app                    # print the project's channel name
projects channel my-app --json             # full resolution (class, linked, source)
projects channel my-app --ensure           # create the channel if it does not exist
# The channel name lives on the project record as
# integrations.conversations_channel; when unset it IS the project slug,
# normalized. This CLI adds no prefix of its own and strips none — it does not
# own the fleet naming convention and will not carry a copy of it. (That is a
# statement about authority, not conformance: plenty of registry slugs do not
# match the convention. Repairing those is a project-rename decision, not a
# derivation one.) A project whose channel is not named after its slug says so
# by setting integrations.conversations_channel, which always wins.
#
# Ensure creates the channel but never writes the link back: pinning a derived
# name would outrank derivation forever and survive a revert. The link is set at
# project creation, or deliberately by an operator.
#
# The channel class comes from the project record too:
# integrations.conversations_channel_class if set, else the project kind, else
# unset — in which case no --class is sent and conversations picks the default.
# Most projects (kind `generic`) fall in that last bucket.
# `projects create` and `projects start` ensure the channel automatically
# (disable with PROJECTS_CHANNEL_ENSURE=0); MCP tool: projects_channel
# A newly created coding-agent session also posts an online notice to this
# channel so subscribed agents see it in chat. This is enabled by default;
# disable it with PROJECTS_AGENT_ONLINE_NOTIFICATIONS=0. Reusing an existing
# session does not post another online notice.

# Shell completion, including workon
eval "$(projects completion)"
eval "$(projects completion --shell zsh)"

Goal-continue Cursor hook (ralph-style)

A stop hook lives in .cursor/hooks.json + .cursor/hooks/goal-continue.sh. When an agent stops, it checks for an active goal and, if incomplete, blocks the stop with a continuation prompt that includes projects next suggestions — modeled on the codewith /goal slash command but driven as a Cursor hook.

Goal sources (first wins): ./.hasna/goal.md, $HASNA_GOAL_FILE, ~/.hasna/goal.md. Mark a goal complete by adding a <!-- done --> line.

Env knobs: HASNA_GOAL_SKIP=1 (disable), HASNA_GOAL_CONTINUE=0 (status only, no continuation), HASNA_GOAL_MAX_SUGGESTIONS=N. The hook never fails closed.

Projects stores high-level management fields directly on the project record: stage, priority, owner, launch_profile, start_agent, start_command, start_session_policy, start_windows, todos links, and brief links. projects start and projects status use those launch defaults unless the command passes an explicit override. By default, projects start detects the current repo/project, creates or reuses the project tmux session, and ensures base windows named 01 and 02. Window 01 is the managed coding-agent/work window when a start command is launched; 02 is the secondary workspace. Existing unrelated tmux windows are left alone. Pass --windows-json or the MCP/API windows field to request the exact tmux window names for a single start/status operation.

Machine-readable outputs for projects list, projects show/projects get, projects status, projects start, projects sessions, projects roots list, and projects recipes list can emit validated JSON Render specs with --render-spec. Existing --json payloads preserve their operational fields and render specs are available from --render-spec and matching projects_render_* MCP tools. Claude starts are annotated with --name when safe. Codewith, Cursor, and OpenCode rename support is reported as manual or unsupported unless a stable programmatic rename path is available; Hasna Projects does not force text into unknown panes. Use projects start --rename-report or projects sessions <project> --unrenamed to inspect rename status. Detailed execution still belongs in todos; long-form specs and decisions still belong in brief.

Workspace Store

Projects has one canonical physical workspace store under HASNA_PROJECTS_HOME, defaulting to ~/.hasna/projects (resolved through the @hasna/paths resolver to the XDG data home ~/.local/share/hasna/projects once the store has been migrated there or HASNA_DATA_HOME is set):

  • canonical workspace path: $HASNA_PROJECTS_HOME/workspaces/<workspace_id>/
  • runtime data path: $HASNA_PROJECTS_HOME/data/<workspace_id>/
  • common runtime children: project.db, logs/, artifacts/, and context/

Rootless new projects default to the canonical workspace path. Explicit --path, registered roots, imports, and GitHub checkout roots keep their requested paths for compatibility until a user runs an explicit store migration. Slugs, names, and org labels are mutable metadata and never define the canonical folder name.

projects store inspect reports the canonical workspace/data paths and whether the current primary path is canonical. projects store ensure creates missing workspace/data directories, initializes the machine-local project.db, and only sets the canonical path as primary when the project had no primary path. On the hosted backend, ensure requires the complete stable wks_... id, reads the full project through the producer-bounded guarded endpoint, and uses the guarded conditional update/receipt path for a missing primary path. Slugs and partial ids are refused before transport; the station-local store is never created from an unbounded or mismatched registry response. projects store migrate remains local-only and is dry-run by default; it requires --apply or --yes to move an existing primary folder into workspaces/<id>, writes a migration plan under data/<id>, preserves git history by moving the directory, records the old path as a non-primary location, updates workspaces.primary_path through the normal location API, rewrites the marker, and verifies the canonical primary path exists.

Labels are project metadata/query filters stored in the existing normalized tag list. Use labels such as org:hasnaxyz, kind:work-project, and client:foo; they do not create canonical folders and are safe to add, remove, or rename.

Per-project app data lives in the canonical runtime data path at $HASNA_PROJECTS_HOME/data/<workspace_id>/project.db. That project database stores project-specific canvases, custom JSON data models/records, and OpenLoops links. projects canvases * --render-spec emits a JSON Render contract for a TypeScript React surface using Tailwind, shadcn components, and React Flow as an infinite canvas; a project may have multiple canvases. Use projects canvases upsert <project> --slug <slug> when an agent or script needs idempotent create-or-update behavior for raw React Flow nodes/edges. Use projects canvases compose <project> for higher-level generic block specs: blocks become ProjectCanvasCard nodes, links become React Flow edges, and table-like blocks can carry columns/rows without creating a one-off domain-specific command.

Generic block specs are intentionally domain-neutral. The same shape can model a reporting hierarchy, a project map, a directory table, a roadmap, or a handoff board:

{
  "slug": "directory",
  "name": "Directory",
  "layout": { "direction": "grid", "columns": 2 },
  "blocks": [
    {
      "id": "summary",
      "title": "Summary",
      "kind": "summary",
      "metrics": [{ "label": "People", "value": 2, "tone": "info" }]
    },
    {
      "id": "table",
      "title": "Directory Table",
      "kind": "table",
      "columns": ["name", "role"],
      "rows": [{ "name": "Ada", "role": "Lead" }]
    }
  ],
  "links": [{ "source": "summary", "target": "table", "label": "details" }]
}

Hosted-backend support uses the same connection rule as Storage Sync. By default the client reads and writes the local SQLite registry (HASNA_PROJECTS_DB_PATH or ~/.hasna/projects/projects.db). When HASNA_PROJECTS_API_URL and HASNA_PROJECTS_API_KEY are both set, all registry reads and writes go to the hosted API instead; this does not move per-project canvases, data records, loop links, or asset files out of $HASNA_PROJECTS_HOME/data/<workspace_id>/. Setting only one API variable fails closed. See the Storage Sync section and docs/hosted-backend-readiness-contract.md for the connection contract.

projects dashboard * is the Projects-owned viewer surface for agent-managed project folders. It standardizes .hasna/project/ inside the project path, collects provider panels from todos, files, mailery, conversations, knowledge, mementos, and reports, adds a read-only actions panel, validates the result as hasna.project_snapshot.v1, and renders it as a React Flow Canvas. Snapshot, render, and validate are read-only unless --write is passed. projects dashboard serve <project> serves /dashboard plus JSON APIs with an HTTP-only same-origin cookie; binding to a non-loopback host requires --token, PROJECTS_DASHBOARD_TOKEN, or an explicit --trust-network choice. Do not put dashboard access tokens in URLs, render specs, reports, or task evidence. Dashboard JSON never carries raw private document bodies or arbitrary shell commands.

projects reports serve serves registered project report files from each project reports/ directory. It binds to 127.0.0.1 by default. Binding reports to a non-loopback host requires --token, PROJECTS_REPORTS_TOKEN, or an explicit --trust-network choice; token mode uses an HTTP-only same-origin cookie or Authorization: Bearer header and does not accept tokens in URLs.

OpenLoops integration uses the @hasna/loops SDK as an optional peer. Runtime commands that need live loop state load @hasna/loops/sdk dynamically, so Open Projects can still manage projects when OpenLoops is not installed.

OSS Routing Matrix

projects oss matrix emits a compact routing matrix for direct child repositories under an OSS workspace root. It is designed for orchestration prompts and dispatch loops that need a capped, machine-readable snapshot without walking an entire monorepo tree.

projects oss matrix \
  --root /home/hasna/Workspace/hasna/opensource \
  --prefix open- \
  --json

Each row includes the repo name/path, package name/version/bin metadata from package.json, git branch/dirty/ahead/behind/remote state, tmux session/window hints, latest task refs from todos, and latest pull request refs from gh when those tools are available. The default limit is 25 repos, with an enforced maximum of 200. Use --limit, --no-prs, --no-tasks, --no-tmux, and --timeout-ms to keep routing scans fast in large workspaces or offline contexts.

Storage Sync

Projects reads and writes either the local SQLite registry or the hosted HTTP API. The client selects the connection from API URL and API key presence.

Local is the default. The client stores the project registry in SQLite at HASNA_PROJECTS_DB_PATH (or ~/.hasna/projects/projects.db) and never touches Postgres.

Set both variables to route all registry reads and writes to the hosted API:

export HASNA_PROJECTS_API_URL="<base URL of the projects server>"  # /v1 is appended
export HASNA_PROJECTS_API_KEY="<API key with projects:read and projects:write>"

The client has one local connection and one HTTP connection. The HTTP connection is selected only when HASNA_PROJECTS_API_URL and HASNA_PROJECTS_API_KEY are both present. The unprefixed keys PROJECTS_API_URL and PROJECTS_API_KEY are accepted as aliases.

On the HTTP connection every registry command goes to <API_URL>/v1 with the API key as a bearer token. The client carries only the API key — never a database DSN — and the key value is never logged, returned, or embedded in output.

Configuration is fail-closed. Setting only one of the API URL and API key refuses to route, and commands hard-fail instead of silently reading the local dataset. Setting neither uses the local SQLite registry.

The HTTP connection moves the global project registry only. Machine-local side effects (tmux sessions, git operations, directory creation, rendering) and per-project data stay on-box either way: canvases, data records, loop links, and asset files live in $HASNA_PROJECTS_HOME/data/<workspace_id>/.

The server side of the hosted connection is projects-serve. It runs against PostgreSQL and requires HASNA_PROJECTS_DATABASE_URL (or PROJECTS_DATABASE_URL / DATABASE_URL), failing fast without it. API keys are verified per request and scoped: projects:read for reads, projects:write for writes. See the HTTP API section below for endpoints and key issuance.

projects create --dry-run is a true no-write creation plan. It returns planned DB writes, filesystem actions, tmux actions, verification steps, locks, and rollback records without writing rows or files. Creation cleanup remains available through MCP as projects_cleanup_create, removing only safe creation artifacts such as the project row, marker file, .git, and empty created directory.

MCP Server

Add to an MCP client config:

{
  "mcpServers": {
    "projects": {
      "command": "projects-mcp"
    }
  }
}

Streamable HTTP (MCP)

MCP uses stdio by default. A long-lived Streamable HTTP transport is also available on 127.0.0.1:

projects-mcp --http              # default port 8871
MCP_HTTP=1 MCP_HTTP_PORT=8871 projects-mcp

Endpoints: GET /health{"status":"ok","name":"projects"}, MCP at POST/GET /mcp.

Project Tools

| Tool | Purpose | | --- | --- | | projects_roots_list / projects_roots_add / projects_roots_show / projects_roots_update / projects_roots_delete / projects_roots_match | Register, inspect, score, update, and delete root folders/path templates | | projects_recipes_list / projects_recipes_add | Manage recipe defaults for project creation | | projects_agents_list / projects_agents_add | Register human, CLI, service, and AI agents | | projects_tmux_profiles_list / projects_tmux_profiles_add / projects_tmux_profiles_apply | Manage reusable tmux sessions/windows | | projects_list / projects_show | Search and inspect projects | | projects_render_list / projects_render_show / projects_render_start / projects_render_status / projects_render_sessions / projects_render_roots / projects_render_recipes | Emit validated JSON Render specs for project surfaces | | projects_store_inspect | Inspect canonical project storage and the per-project app store under $HASNA_PROJECTS_HOME/data/<workspace_id>/project.db | | projects_canvases_list / projects_canvases_create / projects_canvases_upsert / projects_canvases_compose / projects_render_canvas | Manage, compose, update, and render per-project React Flow canvas records | | projects_loops_link / projects_loops_list | Link project stores to OpenLoops loops and summarize them through @hasna/loops | | projects_locations_list / projects_locations_add | Inspect and register additional folder locations for a project | | projects_create | Plan or create a project anywhere on disk | | projects_start | Open or reuse a tmux session, ensure default 01/02 windows, and launch Codewith, Claude, OpenCode, Cursor, or no tool, with optional exact tmux windows | | projects_tmux_status | Inspect expected and current tmux session/window status for a project | | projects_cleanup_create | Clean up DB/files created by a project creation run using rollback records | | projects_import / projects_scan_local_roots | Import existing folders as projects | | projects_import_github / projects_scan_roots / projects_sync_roots | Import GitHub repos as local or remote-only projects, including configured GitHub root scans/syncs | | projects_github_publish / projects_github_unpublish | Publish/unlink GitHub project metadata | | projects_link | Merge external service IDs into project integrations | | projects_unlink | Clear external service IDs from project integrations | | projects_update | Update project metadata with audit events | | projects_tag / projects_untag | Add or remove project tags without replacing the full tag list | | projects_archive / projects_unarchive / projects_delete | Change project lifecycle status | | projects_doctor | Validate markers, paths, locations, references, and failed runs | | projects_events_list / projects_event_record | Inspect or record project audit events | | projects_lock / projects_unlock / projects_locks | Coordinate project mutations | | projects_agent_eval | Run prompt-agent eval cases and return success/confidence | | projects_agent_prompt | Run the AI SDK/OpenRouter project agent loop | | projects_context / projects_next / projects_why / projects_handoff | Agent-assist bundles: orientation, next-action suggestions, resolution trace, cross-agent handoff | | projects_runs_list / projects_runs_show | Read the prompt-agent run ledger (list + full detail with tool-call trace) |

Workspace-named MCP aliases are removed from the public contract.

Prompt Mode

Prompt mode uses AI SDK with OpenRouter. Configure the key with OPENROUTER_API_KEY, PROJECTS_OPENROUTER_API_KEY, or the local secrets vault. The default model is openai/gpt-4o-mini.

Mutations require --yes. Without approval, mutating tools return structured plans/previews. --dry-run forces no-write behavior even if --yes is present.

The prompt agent can inspect roots, recipes, agents, tmux profiles, and projects; create/update/tag/untag/archive/delete/import projects; import/publish GitHub repos; link/unlink external integrations; start projects with saved launch defaults; and plan/apply tmux profiles. It records agent runs and tool calls in SQLite.

projects agent-eval seeds temporary project fixtures into an isolated SQLite database under the eval base path and runs a repeatable prompt suite over root registration/matching, recipe and agent planning, project listing/show/events, create/deduplication, import/scan, update, archive/unarchive, delete/hard-delete, cleanup, verification, tmux planning, GitHub publish/unpublish/import, and integration linking. Live mode uses OpenRouter; --mock runs deterministic create-path coverage and skips live-only cases. The JSON summary reports success_rate, confidence, and db_path.

Normal projects list output hides prompt-agent eval fixtures. Use projects list --include-evals to inspect old fixtures and projects cleanup-evals --dry-run --json followed by projects cleanup-evals --apply to remove them.

Compact output defaults

Human terminal output is compact by default to avoid filling agent context with large records. List/history commands cap rows, truncate long text and paths, and print a hint for the next detail command. Use --limit <n> to raise the row cap, --verbose for extra table columns, full paths, and diagnostic checks, show/events list for detail workflows, and --json for stable machine-readable records.

Compact terminal defaults cover the noisy project registry commands plus smaller registry lists such as projects roots list, projects recipes list, projects agents list, projects tmux-profiles list, projects locks, projects locations list, projects budgets list, and projects doctor. Examples:

projects list --limit 25
projects roots list --limit 10 --verbose
projects doctor --limit 20 --verbose
projects events list my-app --limit 10 --verbose

MCP tools keep their existing full-record defaults for client compatibility. Where supported, pass compact: true to receive compact summaries; compact MCP calls accept limit, and verbose: true returns full records. Prompt-agent tools use compact project/event summaries by default; agent tools expose verbose: true for explicit detail retrieval.

Data Model

Core internal tables:

  • roots: named base folders, tags, path templates, default kind/recipe/tmux profile, GitHub defaults
  • recipes: reusable creation metadata, variables, default tags, and scaffold steps
  • agents: human, CLI, service, and AI actors with provider/model/permissions metadata
  • workspaces: storage records backing projects with kind, status, root, recipe, path, tags, integrations, and metadata
  • workspace_locations: machine-local paths for a project
  • workspace_events: immutable project audit events for mutations and runtime actions
  • agent_runs: prompt-loop run ledger with tool calls and results
  • tmux_profiles / tmux_profile_windows: reusable tmux session/window templates
  • workspace_locks: short-lived mutation locks
  • workspace_migration_map: one-time legacy project-to-workspace mapping

Global registry DB path: ~/.hasna/projects/projects.db (derived from the projects home, so it follows the @hasna/paths resolver once the XDG data home is adopted)

Per-project app data path: ~/.hasna/projects/data/<workspace_id>/project.db

Hosted-backend readiness contract: docs/hosted-backend-readiness-contract.md

Per-project app tables:

  • project_canvases: React Flow-compatible dashboard/canvas records
  • project_data_models: custom JSON data model definitions and render hints
  • project_data_records: custom per-model JSON records
  • project_loop_links: links to @hasna/loops loop ids/names

Global registry override: HASNA_PROJECTS_DB_PATH

Per-project app store root override: HASNA_PROJECTS_HOME

Projects home: ~/.hasna/projects

Override: HASNA_PROJECTS_HOME

SDK

The public SDK exports project-named functions and types. Storage-layer modules still use workspace names internally, but the package boundary is project-first.

import {
  createProject,
  updateProject,
  planProjectCreation,
  executeProjectCreation,
  runProjectAgentPrompt,
  startProject,
  ensureProjectStore,
  ensureDefaultProjectCanvas,
  listProjectCanvases,
  linkProjectLoop,
} from "@hasna/projects";

Per-project store helpers are also available from @hasna/projects/project-store.

HTTP API (projects-serve) & hosted SDK

projects-serve is the HTTP surface for @hasna/projects. It talks directly to PostgreSQL with no local cache or sync in the service.

# apply migrations, then serve
HASNA_PROJECTS_DATABASE_URL=<postgres-connection-string> projects-serve migrate
HASNA_PROJECTS_DATABASE_URL=<postgres-connection-string> HASNA_PROJECTS_API_SIGNING_KEY=<signing-secret> projects-serve   # :8080

Endpoints:

  • GET /health, GET /ready, GET /version{status, version} (unauthenticated)
  • GET /openapi.json → the OpenAPI 3.1 document
  • /v1/* (API key required, header x-api-key or Authorization: Bearer):
    • GET|POST /v1/projects, GET|PATCH|DELETE /v1/projects/{id}, POST /v1/projects/{id}/archive|unarchive, GET /v1/projects/{id}/events
    • GET|POST /v1/roots, GET|PATCH|DELETE /v1/roots/{id}
    • GET|POST /v1/agents, GET /v1/agents/{id}
    • GET|POST /v1/recipes, GET /v1/recipes/{id}

Reads require the projects:read scope; writes require projects:write. Issue a key with contracts issue-key --app projects --scopes projects:*.

The typed client is generated from the serve OpenAPI (bun run sdk:generate):

import { ProjectsClient, createProjectsClientFromEnv } from "@hasna/projects/sdk";

// Hosted connection: PROJECTS_API_URL + PROJECTS_API_KEY (never a DSN)
const projects = createProjectsClientFromEnv();
const created = await projects.createProject({ name: "My Project", tags: ["demo"] });
const list = await projects.listProjects({ tag: "demo" });

Architecture

src/
├── cli/
│   ├── index.ts                 # project CLI and prompt entrypoint
│   └── commands/
│       ├── workspaces.ts         # project, roots, recipes, agents, tmux profiles
│       └── completion.ts         # project shell completion
├── db/
│   ├── database.ts               # SQLite init/path resolution
│   ├── project-store.ts          # per-project project.db helpers
│   ├── schema.ts                 # migrations
│   └── workspaces.ts             # workspace/root/recipe/agent/tmux services
├── lib/
│   ├── workspace-agent.ts        # AI SDK/OpenRouter prompt loop
│   ├── workspace-github.ts       # GitHub import/publish and integration linking
│   ├── workspace-plan.ts         # deterministic creation plans/executor
│   ├── workspace-runtime.ts      # directory/git/marker/tmux runtime actions
│   ├── workspace-import.ts       # arbitrary folder and registered-root import scanner
│   ├── workspace-migration.ts    # legacy migration dry-run, backup, and reports
│   └── workspace-doctor.ts       # marker/path/reference validation
├── mcp/
│   └── index.ts                  # project-first MCP server
├── project-store.ts              # per-project store SDK subpath exports
├── types/
│   └── workspace.ts              # internal storage/project domain types
└── index.ts                      # SDK exports

License

Apache-2.0