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

@lotics/cli

v0.204.0

Published

Lotics SDK and CLI for AI agents

Readme

@lotics/cli

CLI and SDK for AI agents to interact with Lotics.

Lotics is an AI-powered operations platform. Through this CLI you can:

  • Manage tables, records, and views (structured data with typed fields)
  • Generate documents from templates (Excel, Word, PDF, Email)
  • Build and run automations — schedules, webhooks, table lifecycle workflows, button actions, app-action workflows; inspect execution and version history
  • Administer the workspace — invite members, manage groups, share resources, transfer ownership, browse connected accounts (OAuth attach is web-only)
  • Create and manage apps, knowledge docs, and files

Capability guides

lotics docs lists every reference the packages installed beside your project ship — each @lotics/* package carries its own, so a doc always describes the version you actually have. lotics docs <area> prints one (lotics docs ai); lotics docs ui prints a package's index. Every answer is capped at a page, so a long doc hands back its section list and lotics docs <area>/<section> — or, for a doc that is one table, lotics docs ui/catalog/Button. Nothing is copied here — the packages, the areas, the titles and the versions are all read at run time, so a doc or a whole package added upstream shows up without upgrading this CLI.

Driving this CLI from an agent? Start at AGENTS.md (node_modules/@lotics/cli/AGENTS.md) — which surface answers which question, the conventions that hold across every command, and the traps. Per-command contracts, flags, exit codes and gotchas are in docs/cli_reference.md; lotics --help is the always-current verb list.

The platform's primary surfaces have dedicated usage guides that ship inside this package (reachable at node_modules/@lotics/cli/docs/*.md once installed):

  • docs/building_an_app.md — building a custom-code app end to end: the sequence the steps go in (clarify → model → types → queries → workflows → screens → ship) and the deploy-free inner loop. Read it once before starting an app.
  • docs/document_templates.md — generate finished documents (PDF, Excel, Word, email) by filling reusable templates: the five template types, the create → generate → chain lifecycle, and the marker capabilities.
  • docs/knowledge_docs.md — the AI's rulebook layer: authoring the workspace facts an agent can't guess, the access-vs-activation model, and the catalog-then-stage retrieval model agents use to pull only the lines they need.

Start from the library, in one command

Install the CLI, then copy a starter — it creates the account, copies the starter into its workspace, deploys its apps, and prints a one-time sign-in link:

curl -fsSL https://lotics.ai/install.sh | bash
lotics setup <starter_id> --email [email protected]

The installer downloads one compiled executable — no Node.js, no npm, and copying a starter needs neither: every app is deployed on Lotics and nothing is written here. Node 18+ comes in only when you pull an app's source to change it (lotics app pull <app_id>) and build it to deploy again.

Add --json for one machine-readable object instead of progress — the organization, the workspace, the app ids, what was created, the sign-in link, and any warnings. Already signed in? Drop --email and lotics setup <apg_id> copies into the account you have.

lotics library list reads both shelves with no account at all. Presets are a trade's model, named by a slug: read one and write a model.json from it — nothing is copied. Packages are apps and the tables they stand on, named apg_…, copied in whole; each row names how many tables and which apps a copy creates, so what you actually manage can be matched against them. lotics library show <slug|apg_id> reads one, with no account either — each table as alias · label, each field as alias:type, and a preset's variants beside the sentence that selects each.

Or describe your own workspace

A model.json names the tables, fields, options, links, views, roles and first rows, and lotics setup builds them. Start from a preset when one is your trade — name it instead of restating it:

{
  "from": "field_service",
  "variants": ["crews"],
  "rename": { "job": { "label": "Jobs", "fields": { "code": "Job no." } } },
  "rows": { "job": [{ "ref": "j1", "fields": { "code": "J-1" } }] }
}

No preset is your trade? Write the full form instead, spelling the tables out. Either way:

lotics scaffold docs                 # how to write one, both forms, with a worked example. Offline
lotics scaffold check model.json     # prove it — every problem at once, offline unless it names a preset
lotics setup model.json --email [email protected]

It creates no apps of its own: build one in the workspace afterwards (lotics docs building_an_app), or name a published package in the file's apply list and it is copied in — bound onto the tables the model just made — as part of the same run. setup refuses a table name your workspace already has; lotics scaffold apply model.json is the additive verb — it adopts that table, adds what the model declares beyond it, and deletes nothing. Because it never renames, the file and the workspace drift: lotics scaffold diff model.json prints exactly where and exits non-zero on any difference, and lotics field rename <table> <field> "<new label>" moves a label on the platform, in the file (--model) and in every app bound to it (--apps) at once. lotics scaffold apply model.json --documents writes only the file cells of the model's rows, onto the records the first run made — rows land only into empty tables, so it is the one way back for attachments that were missed.

The other direction, once a workspace works: lotics scaffold export > model.json prints its tables (or only --tables tbl_a,tbl_b) as that same file, findings on stderr. It is a starting point for the next business, never a source of truth — the labels are this one's.

Install

curl -fsSL https://lotics.ai/install.sh | bash    # macOS, Linux, WSL — no Node.js needed
npm install -g @lotics/cli                        # or, if you already have Node 18+
irm https://lotics.ai/install.ps1 | iex           # Windows PowerShell — no Node.js needed

Update

lotics upgrade updates this CLI in place, running whichever installer this copy came from. By hand: re-run whichever installer you used; npm install -g @lotics/cli@latest updates an npm install.

The CLI checks for updates once per day and prints a note on stderr, naming the right installer, when a new version is available.

Authentication

lotics auth signup — Creates a new Lotics account, organization, workspace, and API key in one step. Sends a magic link email so you can access the web app.

lotics auth signup                                   # interactive prompts
lotics auth signup [email protected] --name "Agent"            # non-interactive

lotics auth login — Signs in an account that already exists, on a machine that holds no key. It does not wait for you. It prints the page to open (also mailed to you) and the code that page must show, then exits. Sign in there if asked, check the code, press Confirm — and run whatever you wanted to run: the next command that needs a credential collects the key before doing its own work.

lotics auth login [email protected]            # prints the page + code, exits — nothing to keep open
# …press Confirm in the browser…
lotics auth whoami                   # picks up the key, then answers

lotics auth login [email protected] --wait     # one command instead: holds the terminal until Confirm
lotics auth login [email protected] --local    # pins this directory to that org (implies --wait)

A command run before you press Confirm names the page and the code again and exits 1; the request is good for 15 minutes, after which the next command says to ask again. Re-running auth login is always safe — it replaces the request, so only the newest code is the live one.

lotics auth web — Send a magic link email to access the web app (requires prior signup or setup).

lotics auth web

lotics auth api-key — Saves an API key (e.g. one created in the Lotics web app). The key belongs to one org, so this registers that org as a profile — run it once per org. Registering a second key adds a profile; it does not overwrite the first.

lotics auth api-key                  # interactive prompt
lotics auth api-key ltk_...          # registers the key's org as a profile (now active)

Run lotics auth logout [<name|id>] to remove a profile (default: the active org), or lotics auth logout --all to wipe the store. Inside a pinned directory the bare form removes the pin, not a profile — name the org to remove its credential.

Organizations

Each saved API key belongs to one org. Register a key per org once, then switch freely — no re-pasting:

lotics org                          # list saved orgs (marks the active one)
lotics org use acme                 # switch active org by name (or org id)
lotics org use "Acme Corp"          # names are case-insensitive

The org id is the identity; the name is the label the key was saved under, and it is yours — renaming the org in the app never moves it, so the scripts scoping by it keep running. lotics auth whoami learns the new name and records it beside the label; both resolve, and lotics org prints label (server: name) while they differ. A name matching two saved orgs is refused rather than guessed; use the id.

Working in parallel (worktrees)

Pin a directory to its own org/workspace so a global org use elsewhere never disturbs it. The pin is a pointer — the key still comes from the global store, so there's nothing to paste:

cd my-worktree
lotics org use acme --local         # writes ./.lotics/config.json { active_org }
lotics workspace select wks_...     # records the workspace in the local pin

Each worktree resolves independently; switching the global default in another shell leaves pinned worktrees untouched. .lotics/ should be gitignored.

Every command that resolves a workspace names its target before it acts — lotics → Starters / Equipment & Projects on stderr, so stdout stays clean for piping. Both halves are NAMES, since the line exists to be read before a destructive write; the workspace's name is cached beside its id the first time any command lists it, and until then the line prints the id and says the name is not known yet. When nothing in the directory or environment chose the org and it came from the machine-wide default, the line says so and prints the --local command to pin — that default is the one another shell can move between two of your commands. lotics auth whoami reports the same resolution on demand, including which source won.

Resolution precedence

--api-key flag  >  LOTICS_API_KEY env  >  LOTICS_ORG env (name|id)
  >  local .lotics/config.json  >  app manifest workspace → its saved profile
  >  global active profile

LOTICS_WORKSPACE (or --workspace <id> / -w) overrides the workspace at any level. For ephemeral or CI use, set LOTICS_API_KEY instead of saving anything.

LOTICS_ORG is resolved once, before any command runs. A value matching no saved credential refuses every verb with one sentence — a read, a write, and a check that needs no credential alike — and the refusal lists the orgs this machine does hold, so it is answerable without another command. It refuses even when a key arrives another way: a variable that scopes the command must not go unread while LOTICS_API_KEY sends the write somewhere else. When the variable resolves AND a key is supplied, the key decides the org and the command says so. An org keeps the name it was saved under: a server-side rename never moves it, the new name resolves as well, and lotics org prints both when they differ.

Diagnostics

Every request identifies the CLI (user-agent: lotics-cli/<version> node/<v> <platform>) and names the command that made it (x-lotics-cli-command: app.workflow.set), so a failure in the server's logs can be traced to the verb and version that produced it. Neither header carries arguments: the command chain stops before any id, path, @file, or JSON payload.

LOTICS_TELEMETRY=1 additionally records the session. Off by default. Set it in your shell profile rather than per command — each invocation is its own process. When set:

  • Requests carry a session id shared by every command in the sitting — under an agent harness it adopts the harness's own session id, otherwise it rolls over after 30 minutes idle (~/.lotics/session.json).
  • Each invocation appends one record to ~/.lotics/telemetry-<host>.ndjson — the command, its exit code, how long it took, and, on a failure, the message that was printed. One spool per Lotics the CLI talks to, so a record made against one instance is only ever sent to that instance. Batches are sent on a later run against the same host; a failed send is retried, never dropped silently.

Arguments contribute a hash and a shape (records[].data.name:string) and nothing else — no values, no file contents, no record data. The hash is the point: two failures in a row with the same hash mean the error message did not tell you enough to fix the call.

Unset, nothing is stored, nothing is sent, and no spool file is created.

Reporting a problem

lotics report '{"goal":"rename a view","actual":"no command does it","wanted":"lotics run update_view"}'
lotics report @report.json       # a long one, from a file
cat report.json | lotics report -   # or from stdin — `-` is required, see below
lotics report                    # the frame, and what is worth reporting

This is the channel for what the records above cannot show. Everything they capture hangs off a command that failed, which leaves out the two things worth the most: a capability that does not exist (no command ran, so nothing was recorded) and a command that exited 0 having done the wrong thing. Also worth sending: an error whose message did not tell you how to fix it, and anything that made authoring slower than it should have been.

A report is a frame, not a paragraph. A log can reconstruct what you ran; nothing can reconstruct what you wanted, and the gap between the two is the report.

| Field | | | |---|---|---| | goal | required | what you were trying to accomplish | | actual | required | what happened instead | | expected | | what you expected — the gap between this and actual is the defect | | tried | | what you already tried, and what it said | | wanted | | what would have unblocked you |

There is no severity or category to pick.

Reading from stdin needs an explicit -. With - the bare form always prints the frame and a piped one waits as long as it takes.

If LOTICS_TELEMETRY=1 is set, the commands from your session attach themselves — don't retype them. It runs whether or not telemetry is on (invoking it is the consent that passive recording needs an opt-in for), posts immediately, and tells you if it did not land.

Do not paste records, file contents, or credentials.

In an app project, lotics app * commands derive the credential from the directory when nothing explicit chose one: the manifest names the app's workspace, and when exactly one saved profile owns that workspace, that profile is used — the machine-wide default is never consulted. An explicit flag, env var, or directory pin still wins, and an org whose profile remembers a different workspace simply falls through to the announced default.

Workspaces

Workspaces live inside the active org. If the org has more than one, select before running tools:

lotics workspace                    # list workspaces in the active org (marks current)
lotics workspace select wks_...     # set the workspace for the active scope (pin or profile)
lotics workspace create "Sales" --timezone America/New_York --currency USD
                                    # create a new workspace (admin only); without the flags it
                                    # takes the org's oldest workspace's zone and currency
lotics workspace settings --currency USD --timezone America/New_York
                                    # change the current workspace's name/currency/zone
lotics workspace delete wks_... --yes  # delete a workspace (admin only; soft delete, recoverable)

Single-workspace organizations auto-select on first use. The selection is remembered per org, so switching back lands where you left off.

CLI

# Discover tools
lotics tools                    # list tools by category with descriptions
lotics tools query_records      # show full description + input schema

# Execute
lotics run query_tables '{}'
lotics run query_records '{"table_id":"tbl_...","field_keys":["name"]}'

# Large args (a knowledge-doc `content`, a bulk update) exceed the OS arg limit —
# read them from a file or stdin instead of an inline arg:
lotics run create_knowledge @args.json
cat args.json | lotics run create_knowledge -          # the trailing - reads stdin
echo '{"table_id":"tbl_..."}' | lotics run query_records -

# Upload files (multiple files and directories supported)
lotics upload ./report.pdf ./data.csv ./documents/

# Generate a file, then download it
lotics run generate_excel_from_template '{"..."}'
lotics download <file_id> -o ./reports/

# What is in the store, newest first — one page, with the command for the next one
lotics file list --limit 50
lotics file delete <file_id>          # refused while anything still references it

# CI / non-interactive (key inline)
LOTICS_API_KEY=ltk_... lotics run query_tables '{}'

# One-off against a saved org/workspace, no switching
LOTICS_ORG=acme LOTICS_WORKSPACE=wks_... lotics run query_tables '{}'

Local .xlsx and .docx files

Edit them by scripting Lotics's own engines, published as @lotics/xlsx and @lotics/docx. Prefer them over xlsx / exceljs / docx from npm: they round-trip faithfully with the Lotics editor, template engine, and formula engine. npm i @lotics/xlsx @lotics/docx, then read the API guide that ships inside the package: lotics docs xlsx / lotics docs docx prints the installed copy (node_modules/@lotics/<pkg>/AGENTS.md on a machine with no CLI). Both packages ship .ts sources as their entry, so run the script under a TypeScript-aware runner.

import { readFile, writeFile } from "node:fs/promises";
import { parseDocx, replaceText, serializeDocx } from "@lotics/docx";

const { doc, count } = replaceText(await parseDocx(await readFile("in.docx")), "{{name}}", "Acme Ltd.");
if (count === 0) throw new Error("{{name}} is not in this document — nothing written");
await writeFile("out.docx", await serializeDocx(doc));

lotics preview <file.xlsx|.docx> draws one of those files to a PNG with the same engines, so a script's output can be looked at rather than guessed at.

Knowledge docs

A file-native surface over the workspace's knowledge docs — the AI's rulebook layer. The body is a Markdown file: create/update read it from your filesystem, get writes it back. See docs/knowledge_docs.md for the model.

lotics knowledge list                                         # catalog: id, name, description (--json)
lotics knowledge create --name "Shipping tariffs" \
  --description "HS-coded rates; searchable by lane and code" \
  --from ./tariffs.md                                         # or --content '<inline>'; prints the new id
lotics knowledge get kdc_... -o ./tariffs.md                  # body → file (omit -o for stdout; --json = full doc)
lotics knowledge update kdc_... --from ./tariffs.md           # send only what changed (--name / --description too)
lotics knowledge rm kdc_...                                   # archive

create / update send the body inline; the server mints and version-chains the content file (and update diffs + resolves concurrency internally — no token to pass). get is the one content-read path, hydrating the body server-side.

Custom-code apps

# Scaffold / pull / deploy a Vite+React+TS app project
lotics app create "Sales Desk"            # scaffold + deploy v1
lotics app pull app_...                    # bootstrap an existing app locally (incl. .lotics/*)
lotics app deploy -m "Add quote drawer"    # typecheck + build + upload a new version
lotics app versions                        # deploy history: version, when, who, -m message (* = live)
lotics app versions app_...                # ...for any app, without pulling it first

# Apply the latest version of the package this app was COPIED from. Additive on
# the schema; a workflow or agent you have edited here is kept and named, and so
# is a field the new version stopped declaring. Redeploys from the published
# dist, so nothing local is sent — pull afterwards to edit the new code.
lotics app upgrade                         # the app this directory's manifest names
lotics app upgrade app_...                 # ...for any app, without pulling it first

# Regenerate .lotics/* WITHOUT a deploy: the .d.ts type companions (always) +
# the runtime app_fields.ts (when authenticated) — F/OPT maps that address
# fields + select options by stable display-name aliases instead of opaque ids.
# `app pull` writes both too (a deploy archive can never carry app_fields.ts);
# use this after a rename, or when a pull ran offline.
lotics app codegen                         # import { F, OPT } from "../.lotics/app_fields"

# Every deploy pre-flight, WITHOUT the build or the version row: the app's own
# typecheck over regenerated .lotics types, agent schemas vs the live app, aliases
# the code calls that nothing bound, undeclared capabilities, query drift. Exits 1 on what a deploy refuses, so CI can gate on it.
lotics app check

# --screens adds the rendered surface: each screen the app's navigation reaches,
# and the first record each one opens onto a PAGE, headless in Chrome at 1280
# and 375, measured against @lotics/ui docs/reviewing.md. --screen and --width
# narrow it while you iterate on one screen.
lotics app check --screens
lotics app check --screens --screen "Đơn hàng" --width 375

# Running an app's alias is a TOOL call, like every other tool. Exits non-zero
# when the RUN failed, not only when the call did.
lotics run run_app_workflow '{"app_id":"app_...","alias":"issueInvoice","inputs":{"record_id":"rec_..."}}'
cat args.json | lotics run run_app_workflow -  # bulk inputs bypass ARG_MAX
# Honest post-run harvest: created records + a paste-ready cleanup plan + the
# caveat (external/notification calls can't be auto-undone; sub-workflows may run).
lotics run run_app_workflow '{...}' --print-created
lotics run run_app_workflow '{...}' --cleanup   # also deletes created records (NOT a rollback)

# Edit workflow bodies as files. `app pull` writes src/workflows/<alias>.ts (the
# faithful server source, wrapped + referencing its .lotics/workflows/<alias>.globals.d.ts);
# edit the body, then push it back through set_app_workflow — the server verifies it
# (a deploy calls the same verb for every alias the project holds ahead of the app, so
# this is the one-alias spelling of it). `app workflow check` runs the server's own
# JS-subset parser + type-check locally, so a body that passes is one `set` will accept
# (`set` still adds name resolution + lint + structural checks, which need the workspace):
lotics app workflow pull                    # rewrite src/workflows/*.ts + globals from the server
lotics app workflow check                   # parse + typecheck every body locally ([alias...] for some)
lotics app workflow diff issueInvoice       # how the local body differs from the one the server runs
lotics app workflow set issueInvoice        # push the edited src/workflows/issueInvoice.ts

# Iterate on a named query WITHOUT a deploy: push package.json#lotics.queries.<alias>
# to apps.queries (server-validated like a deploy). apps.queries is manifest-
# authoritative, so the next `app deploy` re-syncs it — keep the manifest current.
lotics app query set openInvoices           # push package.json#lotics.queries.openInvoices

# Run a bound app agent end-to-end (no deployed UI needed — app row + declaration
# + member auth). Streams progress to stderr; reports the SETTLED run (structured
# output / final text) to stdout; exits 0 only when the run completed.
lotics run run_app_agent '{"app_id":"app_abc","alias":"recognize","input":{"image_file_id":"fil_..."}}'
cat input.json | lotics run run_app_agent -          # inputs via stdin/@file
lotics run run_app_agent '{...}' --json              # full run summary to stdout
# A run that outlives the call's bounded wait keeps going server-side; read it with
lotics run get_app_agent_run '{"app_id":"app_abc","run_id":"run_..."}'

# Dev-link @lotics/ui to a monorepo checkout for ONE command — nothing hand-written is touched
# (the matching tsc `paths` land in the CLI's own .lotics/tsconfig.link.json)
LOTICS_UI_SRC=/abs/monorepo/packages/ui/src lotics app dev
LOTICS_UI_SRC=/abs/monorepo/packages/ui/src lotics app deploy -m "..."   # warns: bundles YOUR kit copy
lotics app dev                             # unset ⇒ @lotics/ui resolves from node_modules again

app codegen puts a table in app_fields.ts when a declared query reads it or a bound workflow writes it — the queries come from package.json#lotics.queries, the written tables from each binding's own table_ids, so a table only a workflow body touches is addressable by alias with nothing to declare.

SDK

import { LoticsClient } from "@lotics/cli";

const client = new LoticsClient({ apiKey: "ltk_..." });

const { result } = await client.execute("query_tables", {});
const upload = await client.uploadFiles(["./report.pdf", "./data.csv"]);
await client.downloadFile(url, "./output.xlsx");
const { tools, categories } = await client.listTools();
const info = await client.getTool("query_records");