@lotics/cli 0.184.0 → 0.185.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -45711,7 +45711,7 @@ function resultSideEffects(result) {
45711
45711
  }
45712
45712
 
45713
45713
  // src/version.ts
45714
- var VERSION = "0.184.0";
45714
+ var VERSION = "0.185.0";
45715
45715
 
45716
45716
  // src/timezone.ts
45717
45717
  function machineTimezone() {
@@ -45998,9 +45998,11 @@ var COMMANDS = [
45998
45998
  " shipping: agent schemas vs the live app, workflow",
45999
45999
  " declarations and BODIES vs what is live, aliases",
46000
46000
  " the code calls but nothing bound, undeclared",
46001
- " capabilities, query drift. Plus the one rule no",
46002
- " deploy runs: a concrete id or an id-shaped",
46003
- " placeholder in source a starter would ship.",
46001
+ " capabilities, query drift. Plus two rules no",
46002
+ " deploy runs: every body typechecked against",
46003
+ " the pulled types, and a concrete id or an",
46004
+ " id-shaped placeholder in source a starter",
46005
+ " would ship.",
46004
46006
  " Exits 1 on any of it, so CI can gate on it",
46005
46007
  " lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body",
46006
46008
  " through set_app_workflow (server verifies)",
@@ -71205,11 +71207,8 @@ function subsetIssues(bodySource, offset) {
71205
71207
  ];
71206
71208
  }
71207
71209
  async function loadProjectTypescript(projectDir) {
71208
- const requireFromProject = createRequire(path8.join(projectDir, "package.json"));
71209
- let resolved;
71210
- try {
71211
- resolved = requireFromProject.resolve("typescript");
71212
- } catch {
71210
+ const resolved = resolveProjectTypescript(projectDir);
71211
+ if (resolved === null) {
71213
71212
  throw new Error(
71214
71213
  `Could not resolve 'typescript' from ${projectDir}. 'lotics app workflow check' type-checks bodies with the app's own compiler \u2014 run 'npm install' (the scaffold ships typescript as a devDependency).`
71215
71214
  );
@@ -71217,6 +71216,14 @@ async function loadProjectTypescript(projectDir) {
71217
71216
  const mod2 = await import(pathToImportUrl(resolved));
71218
71217
  return mod2.default ?? mod2;
71219
71218
  }
71219
+ function resolveProjectTypescript(projectDir) {
71220
+ const requireFromProject = createRequire(path8.join(projectDir, "package.json"));
71221
+ try {
71222
+ return requireFromProject.resolve("typescript");
71223
+ } catch {
71224
+ return null;
71225
+ }
71226
+ }
71220
71227
  function pathToImportUrl(p) {
71221
71228
  const resolved = path8.resolve(p);
71222
71229
  const prefixed = resolved.startsWith("/") ? resolved : `/${resolved}`;
@@ -73079,13 +73086,35 @@ async function appCheck(client, args = {}) {
73079
73086
  warnIfUndescribed(app);
73080
73087
  const unportable = scanProjectForIds(projectDir);
73081
73088
  reportPortabilityIds(unportable);
73082
- if (!nothingPending(pending) || unportable.length > 0) {
73089
+ const bodiesFailing = await countFailingWorkflowBodies(client, projectDir, meta3);
73090
+ if (!nothingPending(pending) || unportable.length > 0 || bodiesFailing > 0) {
73083
73091
  process.exit(1);
73084
73092
  }
73085
73093
  console.error(
73086
- "Checked the app's bindings, capabilities, agent schemas and id portability \u2014 nothing blocking."
73094
+ "Checked the app's bindings, capabilities, agent schemas, workflow bodies and id portability \u2014 nothing blocking."
73087
73095
  );
73088
73096
  }
73097
+ async function countFailingWorkflowBodies(client, projectDir, meta3) {
73098
+ const bound = Object.keys(meta3.workflows ?? {});
73099
+ const typed = bound.filter((alias) => fs8.existsSync(workflowGlobalsPath(projectDir, alias)));
73100
+ const untyped = bound.filter((alias) => !typed.includes(alias));
73101
+ if (untyped.length > 0) {
73102
+ console.error(
73103
+ `\u26A0 Not type-checked, no local types for: ${untyped.join(", ")}. Run 'lotics app codegen' to write .lotics/workflows/<alias>.globals.d.ts.`
73104
+ );
73105
+ }
73106
+ const toCheck = await collectWorkflowBodyChecks(projectDir, meta3, typed, client);
73107
+ if (toCheck.length === 0) return 0;
73108
+ if (resolveProjectTypescript(projectDir) === null) {
73109
+ console.error(
73110
+ `\u26A0 ${toCheck.length} workflow bod${toCheck.length === 1 ? "y" : "ies"} not type-checked \u2014 no 'typescript' in this project's node_modules. Run 'npm install', then 'lotics app workflow check'.`
73111
+ );
73112
+ return 0;
73113
+ }
73114
+ const results = checkWorkflowBodies(await loadProjectTypescript(projectDir), toCheck);
73115
+ printWorkflowCheckResults(projectDir, results);
73116
+ return results.filter((r) => r.issues.length > 0).length;
73117
+ }
73089
73118
  async function pushPendingBindings(client, projectDir, pending) {
73090
73119
  const plan = [
73091
73120
  ...pending.queries.map((alias) => `query ${alias}`),
@@ -73974,6 +74003,18 @@ async function appWorkflowCheck(args) {
73974
74003
  console.error(`App ${meta3.app_id} has no bound workflows to check.`);
73975
74004
  return;
73976
74005
  }
74006
+ const toCheck = await collectWorkflowBodyChecks(projectDir, meta3, aliases, args.client);
74007
+ if (toCheck.length === 0) {
74008
+ console.error("No workflow bodies to check (every bound alias was skipped).");
74009
+ return;
74010
+ }
74011
+ const tsApi = await loadProjectTypescript(projectDir);
74012
+ const results = checkWorkflowBodies(tsApi, toCheck);
74013
+ printWorkflowCheckResults(projectDir, results);
74014
+ const failed = results.filter((r) => r.issues.length > 0);
74015
+ if (failed.length > 0) process.exit(1);
74016
+ }
74017
+ async function collectWorkflowBodyChecks(projectDir, meta3, aliases, client) {
73977
74018
  const toCheck = [];
73978
74019
  for (const alias of aliases) {
73979
74020
  const bodyPath = workflowFilePath(projectDir, alias);
@@ -73994,14 +74035,14 @@ async function appWorkflowCheck(args) {
73994
74035
  }
73995
74036
  for (const { alias, declaration } of staleWorkflowGlobals(projectDir, meta3.workflows ?? {})) {
73996
74037
  if (!toCheck.some((c) => c.alias === alias)) continue;
73997
- if (!args.client) {
74038
+ if (!client) {
73998
74039
  console.error(
73999
74040
  `\u26A0 "${alias}" \u2014 package.json declares inputs/outputs the local types were not built from, and they cannot be refreshed here (no credentials). Checked against the older types, so a newly declared input will read as a type error that set_app_workflow will accept.`
74000
74041
  );
74001
74042
  continue;
74002
74043
  }
74003
74044
  try {
74004
- await refreshWorkflowTypes(args.client, projectDir, meta3.app_id, alias, declaration);
74045
+ await refreshWorkflowTypes(client, projectDir, meta3.app_id, alias, declaration);
74005
74046
  console.error(`Refreshed workflow types for ${alias} \u2014 package.json had moved on.`);
74006
74047
  } catch (err2) {
74007
74048
  console.error(
@@ -74009,15 +74050,7 @@ async function appWorkflowCheck(args) {
74009
74050
  );
74010
74051
  }
74011
74052
  }
74012
- if (toCheck.length === 0) {
74013
- console.error("No workflow bodies to check (every bound alias was skipped).");
74014
- return;
74015
- }
74016
- const tsApi = await loadProjectTypescript(projectDir);
74017
- const results = checkWorkflowBodies(tsApi, toCheck);
74018
- printWorkflowCheckResults(projectDir, results);
74019
- const failed = results.filter((r) => r.issues.length > 0);
74020
- if (failed.length > 0) process.exit(1);
74053
+ return toCheck;
74021
74054
  }
74022
74055
  function printWorkflowCheckResults(projectDir, results) {
74023
74056
  let totalErrors = 0;
@@ -49,7 +49,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
49
49
  | `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script — the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe — a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |
50
50
  | `lotics docs` \| `lotics docs <area>` | The index of the reference docs, **resolved out of the packages installed beside this project** — never carried by this CLI. **Both levels are discovered by looking**: every `@lotics/*` package carrying an `AGENTS.md` or a `docs/` in any `node_modules/@lotics` from the current directory UPWARD (nearest wins, so a hoisted root copy never shadows the one a project's own imports resolve to), and within each, every area it actually ships. Titles come from each file's own `# heading` and the version from the installed `package.json`, so a doc OR a whole package added upstream appears with no change to this CLI, and a skewed install is visible rather than reassuring. A package's index is named after the package (`lotics docs ui`), never `index`. `@lotics/app-sdk`, `@lotics/ui` and `@lotics/cli` sort first as a reading ORDER, not a filter. Both the index and `<area>` print to **stdout** — the index is the payload of a bare `lotics docs`, so `lotics docs | grep -i excel` works — with only the provenance line on stderr, so `lotics docs ai > ai.md` is the doc alone; a name two packages share is refused with both qualified forms (`lotics docs ui/templates`) rather than resolved silently. Needs no auth. Outside a project only `@lotics/cli`'s own resolve, and it says so. |
51
51
  | `lotics report '<json>'` \| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** — `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` — invoking it IS the consent that passive collection needs an opt-in for — but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. |
52
- | `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping. **First, whether this project is even based on the served version** — the one thing a deploy REFUSES outright rather than pushing (the server 409s a stale `prev_version_id`), and the one finding that invalidates every other: a stale tree and the live app are two different apps, so comparing them reports nothing trustworthy. Stale exits 1 naming both versions and stops before the rest; a project with no stamp at all — or an app with no version yet — is a first deploy, not a conflict. `deploy` runs the SAME assertion off the app row it already fetched, so a stale tree fails before it pushes a binding or builds, instead of after the upload arrives and the server 409s. Then: the manifest's agent schemas against the live app row, every binding a deploy would push, aliases the source calls that nothing bound (queries, workflows AND agents), bindings this bundle stopped calling (the same transition — and the same baseline — `deploy` reports, so the two cannot disagree), capability-gated SDK calls the manifest doesn't declare, a missing icon/theme, a missing app `description` (it heads the capability catalog the chat agent reads every turn, and its absence has no other symptom), a `vite.config.ts` that never defines `global`/`__DEV__`, a `window.open` in the app's own source, and an INSTALLED `@lotics/app-sdk` below the version that understands the host's realtime push — read from `node_modules`, not the dependency range, because a caret is minor-locked below 1.0 so `^0.79.x` can never resolve `0.80` and `npm update` does nothing (all three fail ONLY in the deployed app — dev bundles with esbuild and production with rollup, so typecheck, lint, build and `app dev` are all green while react-native-web reads `global.cancelAnimationFrame` as a free variable and the sandboxed iframe drops a popup silently), an agent whose capability and its reach disagree, in EITHER direction, over any of the five declaration-bound tools (`run_app_query`/`run_app_workflow` against `query_aliases`/`workflow_aliases`; `grep_knowledge`/`read_knowledge`/`list_knowledge` against `knowledge_doc_ids`) — the tool is the capability, the list is the reach, and a tool with no reach means every call it makes is refused while the run still COMPLETES, so it surfaces as a model ignoring its prompt; read off the live row, never the manifest, which mirrors those fields but is pushed by no verb, and a notice for any alias the source computes at runtime (invisible to every check here and to `--prune`'s unbind guard). **And whether the kit this app builds against has fallen behind what is published** — `@lotics/ui` and `@lotics/app-sdk`, read from `node_modules` for the same reason as the floor check above: a range keeps accepting, so an app pinned `^44.x` reads healthy for a year, and even an in-range one sits on the lockfile's older patch until `npm update` (never `npm install`, which honours the lock). A MAJOR behind is loud and names the packages actually behind — plus `@lotics/ui`'s `MIGRATION.md`, when ui is one of them, since it is the only half that keeps one; anything smaller is one quiet line, because a warning that fires on every deploy is one the reader stops seeing. The registry lookup is bounded and every failure — offline, slow, private — is silence: a version check must never become a new way for a deploy to fail. Every deploy finding is the same helper `deploy` calls, so a green check means a deploy will not complain. **And the PORTABILITY gate, which is the one rule `check` runs that a deploy does not** — the ids an app cannot carry into another workspace, over the working tree, with the same exclusions the deploy tar applies. Two rules. **An id this workspace MINTED**, written into `src/`, a `.md` or the app's own docs — it resolves to nothing in a copy, and in prose it is an instruction the copier's agent follows; this is the one a `starter publish` also refuses, on the uploaded archive. **And an id-shaped STAND-IN** too short for the generator that mints its prefix (`"opt_X"`, `"fld_a"` — quoted or in a code span, so a bare `opt_in` stays legal, and never in a test file), which only `check` runs, and which additionally reads a workflow body and the manifest: those two are exempt from the first rule because a publish INVERTS a real id there, and it cannot invert a fake — so without this a stand-in survives until the publish resolves it against the app's footprint, on somebody else's machine. Each is reported as `<file>:<line> — <id>` with the one edit that fixes it. This gate reads only the files, but the command around it still needs a resolvable credential and the live app row, so it is not an offline check. **Exits 1 on that and on what a `deploy` would REFUSE or PUSH** — an agent schema that disagrees with the live app, and any binding the project has ahead of the app (an edited workflow body or declaration, edited agent prose, a changed query). Both are things a deploy would act on, so CI gating on a green check means a deploy has nothing left to do; genuine advisories (capabilities, branding, a runtime-computed alias, orphaned bindings) stay advisory and never fail it. |
52
+ | `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping. **First, whether this project is even based on the served version** — the one thing a deploy REFUSES outright rather than pushing (the server 409s a stale `prev_version_id`), and the one finding that invalidates every other: a stale tree and the live app are two different apps, so comparing them reports nothing trustworthy. Stale exits 1 naming both versions and stops before the rest; a project with no stamp at all — or an app with no version yet — is a first deploy, not a conflict. `deploy` runs the SAME assertion off the app row it already fetched, so a stale tree fails before it pushes a binding or builds, instead of after the upload arrives and the server 409s. Then: the manifest's agent schemas against the live app row, every binding a deploy would push, aliases the source calls that nothing bound (queries, workflows AND agents), bindings this bundle stopped calling (the same transition — and the same baseline — `deploy` reports, so the two cannot disagree), capability-gated SDK calls the manifest doesn't declare, a missing icon/theme, a missing app `description` (it heads the capability catalog the chat agent reads every turn, and its absence has no other symptom), a `vite.config.ts` that never defines `global`/`__DEV__`, a `window.open` in the app's own source, and an INSTALLED `@lotics/app-sdk` below the version that understands the host's realtime push — read from `node_modules`, not the dependency range, because a caret is minor-locked below 1.0 so `^0.79.x` can never resolve `0.80` and `npm update` does nothing (all three fail ONLY in the deployed app — dev bundles with esbuild and production with rollup, so typecheck, lint, build and `app dev` are all green while react-native-web reads `global.cancelAnimationFrame` as a free variable and the sandboxed iframe drops a popup silently), an agent whose capability and its reach disagree, in EITHER direction, over any of the five declaration-bound tools (`run_app_query`/`run_app_workflow` against `query_aliases`/`workflow_aliases`; `grep_knowledge`/`read_knowledge`/`list_knowledge` against `knowledge_doc_ids`) — the tool is the capability, the list is the reach, and a tool with no reach means every call it makes is refused while the run still COMPLETES, so it surfaces as a model ignoring its prompt; read off the live row, never the manifest, which mirrors those fields but is pushed by no verb, and a notice for any alias the source computes at runtime (invisible to every check here and to `--prune`'s unbind guard). **And whether the kit this app builds against has fallen behind what is published** — `@lotics/ui` and `@lotics/app-sdk`, read from `node_modules` for the same reason as the floor check above: a range keeps accepting, so an app pinned `^44.x` reads healthy for a year, and even an in-range one sits on the lockfile's older patch until `npm update` (never `npm install`, which honours the lock). A MAJOR behind is loud and names the packages actually behind — plus `@lotics/ui`'s `MIGRATION.md`, when ui is one of them, since it is the only half that keeps one; anything smaller is one quiet line, because a warning that fires on every deploy is one the reader stops seeing. The registry lookup is bounded and every failure — offline, slow, private — is silence: a version check must never become a new way for a deploy to fail. Every deploy finding is the same helper `deploy` calls, so a green check means a deploy will not complain. **And the PORTABILITY gate, which is the one rule `check` runs that a deploy does not** — the ids an app cannot carry into another workspace, over the working tree, with the same exclusions the deploy tar applies. Two rules. **An id this workspace MINTED**, written into `src/`, a `.md` or the app's own docs — it resolves to nothing in a copy, and in prose it is an instruction the copier's agent follows; this is the one a `starter publish` also refuses, on the uploaded archive. **And an id-shaped STAND-IN** too short for the generator that mints its prefix (`"opt_X"`, `"fld_a"` — quoted or in a code span, so a bare `opt_in` stays legal, and never in a test file), which only `check` runs, and which additionally reads a workflow body and the manifest: those two are exempt from the first rule because a publish INVERTS a real id there, and it cannot invert a fake — so without this a stand-in survives until the publish resolves it against the app's footprint, on somebody else's machine. Each is reported as `<file>:<line> — <id>` with the one edit that fixes it. This gate reads only the files, but the command around it still needs a resolvable credential and the live app row, so it is not an offline check. **And every bound workflow body, type-checked locally** — the same isolated per-alias program `app workflow check` builds, against the pulled `.lotics/workflows/<alias>.globals.d.ts`. The server verifies a body once, at the save that wrote it, so a helper whose declared signature has since moved (`toNumber` returning `number | null`) leaves it stored, matching what is live, and refused by the next writer — a starter copy, in somebody else's workspace. The verdict is as fresh as those types, which `pull`, `workflow pull` and `codegen` refresh. **Exits 1 on that, on a body the types refuse, and on what a `deploy` would REFUSE or PUSH** — an agent schema that disagrees with the live app, and any binding the project has ahead of the app (an edited workflow body or declaration, edited agent prose, a changed query). Both are things a deploy would act on, so CI gating on a green check means a deploy has nothing left to do; genuine advisories (capabilities, branding, a runtime-computed alias, orphaned bindings) stay advisory and never fail it. |
53
53
  | `lotics app workflow set <alias>` | Push the edited `src/workflows/<alias>.ts` body through `set_app_workflow` (the single author of `apps.workflows`). Reads the body from disk (header + `/// <reference>` + `export {};` marker + the `__workflow` wrapper all stripped) + the typed `inputs`/`outputs` **and the `description`** from `package.json#lotics.workflows.<alias>`; the **server** re-verifies the body and echoes the bound `outputs` (declared, else DERIVED from `return({ data })`). The `description` is the one line an agent reads when choosing between the app's aliases (the workflow counterpart to a query's) — authored in the manifest so it lives beside the body in version control and rides every push; omit it and the workflow keeps whatever description it already has, so a push can never blank one set elsewhere. When the manifest declared NO `outputs`, the DERIVED echo is written back into `package.json#lotics.workflows.<alias>.outputs` (a SURGICAL write — preserves `knowledge`/`config` and every other manifest field) and that alias's types are refreshed in place, so `useWorkflow("<alias>")`'s `result.data` is typed immediately with no hand-copy and no second `lotics app codegen`; an explicitly-declared `outputs` is authoritative and never overwritten. Deploy still never authors workflows — this is a CLI convenience over the existing tool. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a verify failure. |
54
54
  | `lotics app agent set <alias>` | Push `src/agents/<alias>.md` — plus `inputs`/`outputs` when `package.json#lotics.agents.<alias>` declares them — through `set_app_agent`. The agent mirror of `app workflow set`, and the deploy-free authoring path for an agent's prose and its typed edges. **It sends only those fields.** Everything else is absent, and absent means unchanged, so a declaration this CLI does not model cannot be reverted by a push from a checkout that predates it — the chat authoring agent's `knowledge_doc_ids`, another operator's `query_aliases` grant. To change one of those, call `set_app_agent` with just that field (`lotics run set_app_agent '{"app_id":…,"alias":…,"tool_names":[…]}'` — it merges), then `app pull` to bring the manifest back in step. **CREATES the alias when the app has not bound one yet**, so a new agent is authored the same way a new workflow is: write the prose, declare the typed half, push. A create needs the prose file (an agent without instructions is not an agent); it is gated on nothing else, because what keeps a binding alive is a `useAppAgentRun("<alias>")` call site in the shipped bundle — a deploy prunes an agent the bundle never names, manifest entry or not. The prose push is a conditional write against the fingerprint this project last saw, so it is refused rather than allowed to overwrite prose someone else changed. Clear error + non-zero exit when there is no prose file and nothing declared to push instead, when a create has no prose to create from, or when the file is empty once the header is stripped. |
55
55
  | `lotics app query set <alias>` \| `--all` | Push `package.json#lotics.queries` (`{ ast, params? }` per alias) to `apps.queries` through `set_app_query` — **the only author of a query binding**, the mirror of `app workflow set`. A deploy pushes a DRIFTED declaration through this same verb before it ships (see `app deploy`), so this is the explicit single-alias path, not the only way a query reaches the app. The **server** validates each one exactly as it always did (alias identifier, workspace-only tables, resolvable fields, declared params). `--all` pushes every declared alias, alias-sorted, stopping at the first failure and naming what already landed. Clear error + non-zero exit on an alias absent from the manifest or a validation failure. **The declaration's fields MERGE**, so the manifest is not a snapshot: deleting `params` from an alias and pushing leaves the live params exactly where they were, because an absent key means "unchanged". Clear one with `params: null`, or replace the map with the set you want. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.184.0",
3
+ "version": "0.185.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {