luxin-cli 0.2.7 → 0.2.8

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/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ provenance; this file is the human- and agent-readable release map.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.2.8 - 2026-08-12
10
+
11
+ - New command: `luxin skill` prints the full usage guidance from bundled
12
+ package bytes, so instructions always match the CLI you are running. The
13
+ installed SKILL.md is now a ~4KB discovery stub that points here; the full
14
+ body is also served at https://luxin.sh/skill-core.md.
15
+ - Skill freshness handshake: export `LUXIN_SKILL_REVISION=<the revision in
16
+ your stub's frontmatter>` and `create --guide`, `edit --guide`, and
17
+ `luxin skill` will return `data.skill_refresh` with the current revision and
18
+ the exact refresh command (`npx skills update luxin`) whenever your installed
19
+ copy no longer matches. The field is omitted entirely when you are current.
20
+ Nothing ever rewrites your files, config, or hooks — refresh is always an
21
+ explicit command you run.
22
+ - `agent_skill.read_command` now points at `luxin skill --json` instead of
23
+ curling the stub.
24
+
9
25
  ## 0.2.7 - 2026-08-12
10
26
 
11
27
  - Release (pricing): the guide's local credit quoting for xAI models moved from
package/README.md CHANGED
@@ -85,9 +85,67 @@ Naming note: Luxin is a creative-media runtime for agents. It is unrelated to
85
85
  `luxon`, the datetime library; the npm package here is `luxin-cli` and the
86
86
  installed binary is `luxin`.
87
87
 
88
+ ## Keeping The Skill Current
89
+
90
+ The installed `SKILL.md` is a ~4KB discovery stub. Everything volatile — flags,
91
+ workflows, recovery, funding — lives in the guidance the CLI itself carries, so
92
+ it always matches the code you are running:
93
+
94
+ ```bash
95
+ npm_config_update_notifier=false npx -y luxin-cli@latest skill
96
+ ```
97
+
98
+ Refresh the installed stub with the standard verb:
99
+
100
+ ```bash
101
+ npx skills update luxin
102
+ ```
103
+
104
+ The stub records its own identity as `metadata.skill_revision`. Export it and
105
+ the CLI will say when your copy no longer matches the guidance it ships,
106
+ returning `data.skill_refresh` plus one warning line on `luxin skill` and
107
+ `create --guide`:
108
+
109
+ ```bash
110
+ export LUXIN_SKILL_REVISION=<metadata.skill_revision from your installed SKILL.md>
111
+ ```
112
+
113
+ Detection is remote; mutation stays local and explicit. Luxin prints the refresh
114
+ command and never rewrites your skill files, agent config, or hook files.
115
+
116
+ ### Optional: refresh at session start (copy-paste, opt-in)
117
+
118
+ Some runtimes can run a command when a session starts. If you want an automatic
119
+ refresh, add it yourself — Luxin will not install it for you, and nothing below
120
+ runs unless you put it there. For Claude Code, in `.claude/settings.json`:
121
+
122
+ ```json
123
+ {
124
+ "hooks": {
125
+ "SessionStart": [
126
+ {
127
+ "hooks": [
128
+ {
129
+ "type": "command",
130
+ "command": "npx skills update luxin",
131
+ "timeout": 60
132
+ }
133
+ ]
134
+ }
135
+ ]
136
+ }
137
+ }
138
+ ```
139
+
140
+ Two things worth knowing before you adopt it: it adds a network call to every
141
+ session start, and it applies updates without showing you a diff first. Running
142
+ `npx skills update luxin` by hand when a nudge appears keeps the change
143
+ reviewable.
144
+
88
145
  Agent-facing contracts:
89
146
 
90
147
  - [Hosted skill contract](https://luxin.sh/skill.md)
148
+ - [Hosted skill guidance](https://luxin.sh/skill-core.md)
91
149
  - [Hosted LLM contract](https://luxin.sh/llms.txt)
92
150
  - [Hosted CLI contract](https://luxin.sh/cli.md)
93
151
  - [Public repo skill source](https://github.com/danielgwilson/luxin/tree/main/skills/luxin)
package/bin/luxin.mjs CHANGED
@@ -15,7 +15,20 @@ import { Readable } from "node:stream";
15
15
  import { pipeline } from "node:stream/promises";
16
16
  import os from "node:os";
17
17
 
18
- const VERSION = "0.2.7";
18
+ const VERSION = "0.2.8";
19
+ // Content-derived identity of the guidance this package ships (#2302). Kept in
20
+ // sync with docs/public-contract/skill.md by `pnpm skill-revision:write` and
21
+ // gated by `pnpm skill-revision:check`; the mirror cannot import from src/.
22
+ const SKILL_REVISION = "b1dc1284d18d";
23
+ // `luxin skill` prints these bundled bytes rather than fetching them, so the
24
+ // guidance can never describe a CLI other than the one printing it. Packed by
25
+ // public-cli/package.json `files`.
26
+ const SKILL_GUIDANCE_FILE = "skill-core.md";
27
+ const SKILL_GUIDANCE_SCHEMA = "image-skill.skill-guidance.v1";
28
+ const SKILL_CORE_URL = "https://luxin.sh/skill-core.md";
29
+ const SKILL_REFRESH_COMMAND = "npx skills update luxin";
30
+ const INSTALLED_SKILL_REVISION_ENV = "LUXIN_SKILL_REVISION";
31
+ const INSTALLED_SKILL_REVISION_HEADER = "x-luxin-skill-revision";
19
32
  const PACKAGE_NAME = "luxin";
20
33
  const DEFAULT_API_BASE_URL = "https://api.luxin.sh";
21
34
  const DEFAULT_DOCS_BASE_URL = "https://luxin.sh";
@@ -334,7 +347,8 @@ function agentSkillHint(commandPrefix = "luxin") {
334
347
  url: AGENT_SKILL_URL,
335
348
  llms_url: AGENT_LLMS_URL,
336
349
  cli_docs_url: AGENT_CLI_DOCS_URL,
337
- read_command: "curl -fsSL https://luxin.sh/skill.md",
350
+ // #2302: point at the CLI-carried guidance, not the discovery stub.
351
+ read_command: "luxin skill --json",
338
352
  recommended_start_command: renderGuidePrefixedCommand(
339
353
  commandPrefix,
340
354
  "create --guide --prompt PROMPT --json",
@@ -387,6 +401,8 @@ async function main(rawArgv) {
387
401
  return await doctor(rest);
388
402
  case "trust":
389
403
  return await trust(rest);
404
+ case "skill":
405
+ return await skill(rest);
390
406
  case "signup":
391
407
  return await signup(rest);
392
408
  case "claim":
@@ -503,12 +519,13 @@ function commandHelpByKey(key) {
503
519
  "": {
504
520
  command: "help",
505
521
  usage:
506
- "luxin <doctor|trust|signup|claim|whoami|usage|quota|credits|capabilities|models|create|upload|edit|assets|jobs|activity|feedback> --json",
522
+ "luxin <doctor|trust|skill|signup|claim|whoami|usage|quota|credits|capabilities|models|create|upload|edit|assets|jobs|activity|feedback> --json",
507
523
  docs_url: "https://luxin.sh/cli.md",
508
524
  agent_skill: agentSkillHint(),
509
525
  commands: [
510
526
  "doctor",
511
527
  "trust",
528
+ "skill",
512
529
  "signup --agent --agent-name NAME --runtime RUNTIME",
513
530
  "claim request --contact INBOX",
514
531
  "claim code",
@@ -560,6 +577,13 @@ function commandHelpByKey(key) {
560
577
  description:
561
578
  "Return npm provenance, hosted contract hashes, API health, and model availability evidence.",
562
579
  },
580
+ skill: {
581
+ command: "luxin skill help",
582
+ usage: "luxin skill --json",
583
+ docs_url: "https://luxin.sh/cli.md#luxin-skill",
584
+ description:
585
+ "Print the full agent guidance bundled with this CLI version, so instructions always match the installed code. No network call, no auth, no spend.",
586
+ },
563
587
  signup: {
564
588
  command: "luxin signup help",
565
589
  usage: "luxin signup --agent --agent-name NAME --runtime RUNTIME --json",
@@ -880,6 +904,104 @@ function unsupportedFlagsMessage(command, flags) {
880
904
  .join(", ")}`;
881
905
  }
882
906
 
907
+ // #2302: the installed SKILL.md is a ~4KB discovery stub. Everything volatile
908
+ // -- flags, workflows, recovery, funding -- lives in the guidance body this
909
+ // command prints, so an agent working from a months-old install still reads
910
+ // instructions that match the code it is about to run.
911
+ //
912
+ // Bundled, not fetched. A network fetch could serve prose this build does not
913
+ // implement, which is the drift the split exists to remove.
914
+ async function skill(argv) {
915
+ const args = parseArgs(argv);
916
+ const unsupportedFlags = [...args.flags.keys()].filter(
917
+ (flag) => !["json"].includes(flag),
918
+ );
919
+ if (args.positionals.length > 0 || unsupportedFlags.length > 0) {
920
+ return invalid(
921
+ "luxin skill",
922
+ unsupportedFlags.length > 0
923
+ ? unsupportedFlagsMessage("skill", unsupportedFlags)
924
+ : "skill does not accept positional arguments",
925
+ );
926
+ }
927
+
928
+ let content;
929
+ try {
930
+ content = await readFile(
931
+ new URL(`../${SKILL_GUIDANCE_FILE}`, import.meta.url),
932
+ "utf8",
933
+ );
934
+ } catch (error) {
935
+ return failure(
936
+ "luxin skill",
937
+ 7,
938
+ "SKILL_GUIDANCE_UNAVAILABLE",
939
+ `bundled skill guidance could not be read: ${
940
+ error instanceof Error ? error.message : "unknown error"
941
+ }. Reinstall the CLI, or read ${SKILL_CORE_URL}`,
942
+ false,
943
+ );
944
+ }
945
+
946
+ const refresh = skillRefresh();
947
+ return success(
948
+ "luxin skill",
949
+ {
950
+ schema: SKILL_GUIDANCE_SCHEMA,
951
+ revision: SKILL_REVISION,
952
+ source: "bundled_cli_package",
953
+ format: "markdown",
954
+ bytes: Buffer.byteLength(content, "utf8"),
955
+ canonical_url: SKILL_CORE_URL,
956
+ refresh_command: SKILL_REFRESH_COMMAND,
957
+ mutation: {
958
+ provider_call: false,
959
+ credit_debit: false,
960
+ media_write: false,
961
+ local_file_write: false,
962
+ },
963
+ ...(refresh === null ? {} : { skill_refresh: refresh }),
964
+ content,
965
+ },
966
+ refresh === null ? [] : [skillRefreshWarning(refresh)],
967
+ );
968
+ }
969
+
970
+ /**
971
+ * Self-reported identity of the caller's installed skill copy. 12 lowercase hex
972
+ * or nothing: the value is echoed into a response field and forwarded as a
973
+ * header, so it must not become a channel for arbitrary text.
974
+ */
975
+ function installedSkillRevision() {
976
+ const raw = process.env[INSTALLED_SKILL_REVISION_ENV];
977
+ if (typeof raw !== "string") {
978
+ return null;
979
+ }
980
+ const trimmed = raw.trim().toLowerCase();
981
+ return /^[0-9a-f]{12}$/.test(trimmed) ? trimmed : null;
982
+ }
983
+
984
+ /**
985
+ * The compact nudge, or nothing at all. Omitted when the caller reports nothing
986
+ * or reports the revision this CLI ships. Content hashes are unordered, so this
987
+ * says "differs", never "older".
988
+ */
989
+ function skillRefresh() {
990
+ const installed = installedSkillRevision();
991
+ if (installed === null || installed === SKILL_REVISION) {
992
+ return null;
993
+ }
994
+ return {
995
+ installed_revision: installed,
996
+ current_revision: SKILL_REVISION,
997
+ refresh_command: SKILL_REFRESH_COMMAND,
998
+ };
999
+ }
1000
+
1001
+ function skillRefreshWarning(refresh) {
1002
+ return `installed skill revision ${refresh.installed_revision} does not match the current revision ${refresh.current_revision} this CLI ships; refresh with: ${refresh.refresh_command}`;
1003
+ }
1004
+
883
1005
  async function trust(argv) {
884
1006
  const args = parseArgs(argv);
885
1007
  const unsupportedFlags = [...args.flags.keys()].filter(
@@ -2342,6 +2464,7 @@ async function createGuide(args, options = {}) {
2342
2464
  },
2343
2465
  ),
2344
2466
  };
2467
+ const guideSkillRefresh = skillRefresh();
2345
2468
  const guideData = {
2346
2469
  schema:
2347
2470
  guideOperation === "edit"
@@ -2468,20 +2591,25 @@ async function createGuide(args, options = {}) {
2468
2591
  credit_debit: false,
2469
2592
  media_write: false,
2470
2593
  },
2594
+ // #2302: present only when the caller self-reported a revision that is not
2595
+ // the one this CLI ships. Survives ready_compact deliberately -- a nudge
2596
+ // that only appears in the blocked stages would never reach the agents who
2597
+ // are working fine on stale instructions, which is the whole failure mode.
2598
+ ...(guideSkillRefresh === null ? {} : { skill_refresh: guideSkillRefresh }),
2471
2599
  };
2472
2600
  for (const field of compactOmittedFields) {
2473
2601
  delete guideData[field];
2474
2602
  }
2475
- return createGuideSuccess(
2476
- command,
2477
- quota?.envelope.actor ?? null,
2478
- guideData,
2479
- plannedDebitUnavailable
2603
+ return createGuideSuccess(command, quota?.envelope.actor ?? null, guideData, [
2604
+ ...(plannedDebitUnavailable
2480
2605
  ? [
2481
2606
  "create_credit_debit_unavailable: this hosted API does not publish per-aspect-ratio create debits, so the credits above are the model's default-shape quote and may not be what create debits. Run the dry_run command in escape_hatches for the exact figure before budgeting.",
2482
2607
  ]
2483
- : [],
2484
- );
2608
+ : []),
2609
+ ...(guideSkillRefresh === null
2610
+ ? []
2611
+ : [skillRefreshWarning(guideSkillRefresh)]),
2612
+ ]);
2485
2613
  }
2486
2614
 
2487
2615
  function createGuideSuccess(command, actor, data, warnings = []) {
@@ -6869,6 +6997,12 @@ async function apiRequest(input) {
6869
6997
  ...(input.token === undefined
6870
6998
  ? {}
6871
6999
  : { authorization: `Bearer ${input.token}` }),
7000
+ // #2302: self-reported identity of the installed skill copy, so the
7001
+ // hosted side can measure how stale the installed base is. Non-secret,
7002
+ // omitted entirely when unset, never guessed.
7003
+ ...(installedSkillRevision() === null
7004
+ ? {}
7005
+ : { [INSTALLED_SKILL_REVISION_HEADER]: installedSkillRevision() }),
6872
7006
  },
6873
7007
  body: input.body === undefined ? undefined : JSON.stringify(input.body),
6874
7008
  });
package/cli.md CHANGED
@@ -93,6 +93,63 @@ verified, the command still returns a packet with explicit `unreachable`,
93
93
  `not_available_yet`, `inspect_only`, or `stale_or_mismatched` states rather
94
94
  than omitting the field.
95
95
 
96
+ ### `luxin skill`
97
+
98
+ Prints the full agent guidance that shipped with this CLI version.
99
+
100
+ ```bash
101
+ luxin skill --json
102
+ ```
103
+
104
+ The published `SKILL.md` at `https://luxin.sh/skill.md` is a ~4KB discovery
105
+ stub: it carries identity, the first command, the safety invariants, and a
106
+ revision, and nothing else. Flags, workflows, model parameters, recovery, and
107
+ funding details live here, because an installed skill file is a snapshot that
108
+ never updates while `npx luxin-cli@latest` updates on every call.
109
+
110
+ The body is read from the package this command is running from, not fetched, so
111
+ the guidance always matches the installed code by construction. No network call,
112
+ no auth, no spend. The same bytes are served at
113
+ `https://luxin.sh/skill-core.md`.
114
+
115
+ `data` uses `schema: "image-skill.skill-guidance.v1"` and carries `revision`
116
+ (the 12-hex content-derived identity of the published skill), `source`,
117
+ `format`, `bytes`, `canonical_url`, `refresh_command`, `mutation`, and
118
+ `content`.
119
+
120
+ #### Skill revision and staleness
121
+
122
+ An installed skill file records its revision as `metadata.skill_revision` in its
123
+ frontmatter. Export it so the CLI can tell you when your copy has fallen behind
124
+ the guidance the CLI ships:
125
+
126
+ ```bash
127
+ export LUXIN_SKILL_REVISION=<metadata.skill_revision from your installed SKILL.md>
128
+ ```
129
+
130
+ When the value differs from what the running CLI ships, `luxin skill` and
131
+ `create --guide` / `edit --guide` add `data.skill_refresh`:
132
+
133
+ ```json
134
+ {
135
+ "skill_refresh": {
136
+ "installed_revision": "0123456789ab",
137
+ "current_revision": "b1dc1284d18d",
138
+ "refresh_command": "npx skills update luxin"
139
+ }
140
+ }
141
+ ```
142
+
143
+ They also emit one warning line naming the same command. When the revisions
144
+ match, or when `LUXIN_SKILL_REVISION` is unset, the field is omitted entirely.
145
+
146
+ Refreshing is always a command you choose to run. Luxin never rewrites your
147
+ skill files, agent config, or hooks, and never fetches instructions to follow
148
+ them. The revision is also forwarded to the hosted API as a non-secret
149
+ `x-luxin-skill-revision` header and published unauthenticated on
150
+ `GET /healthz` as `skill_revision`, so a client can compare without downloading
151
+ any contract file.
152
+
96
153
  ### `luxin signup --agent`
97
154
 
98
155
  Bootstraps restricted agent access. Signup is anonymous by default: no contact
@@ -280,6 +337,13 @@ recommended (`data.self_fund_preparation.recommended` is `true`), the
280
337
  `data.self_fund_*` fields stay in the compact response and are not listed in
281
338
  `data.output_mode.omitted_fields`.
282
339
 
340
+ `data.skill_refresh` is the other field that survives the compact response, for
341
+ the same reason: an agent whose installed skill file has gone stale is usually
342
+ an agent whose calls are otherwise succeeding. It appears only when
343
+ `LUXIN_SKILL_REVISION` is exported and differs from the revision the running CLI
344
+ ships, and carries `installed_revision`, `current_revision`, and
345
+ `refresh_command`. See [`luxin skill`](#luxin-skill) for the full handshake.
346
+
283
347
  ```bash
284
348
  IMAGE_SKILL_DISCOVERY_SOURCE=cli-md luxin create --guide --prompt "a compact field camera on a stainless workbench"
285
349
  ```
package/commands.json CHANGED
@@ -20,6 +20,13 @@
20
20
  "mutation": false,
21
21
  "spend": false
22
22
  },
23
+ {
24
+ "id": "skill",
25
+ "docs_anchor": "luxin-skill",
26
+ "surface": "client",
27
+ "mutation": false,
28
+ "spend": false
29
+ },
23
30
  {
24
31
  "id": "signup",
25
32
  "docs_anchor": "luxin-signup-agent",
@@ -148,13 +155,13 @@
148
155
  "key": "",
149
156
  "help": {
150
157
  "command": "help",
151
- "usage": "luxin <doctor|trust|signup|claim|whoami|usage|quota|credits|capabilities|models|create|upload|edit|assets|jobs|activity|feedback> --json",
158
+ "usage": "luxin <doctor|trust|skill|signup|claim|whoami|usage|quota|credits|capabilities|models|create|upload|edit|assets|jobs|activity|feedback> --json",
152
159
  "docs_url": "https://luxin.sh/cli.md",
153
160
  "agent_skill": {
154
161
  "url": "https://luxin.sh/skill.md",
155
162
  "llms_url": "https://luxin.sh/llms.txt",
156
163
  "cli_docs_url": "https://luxin.sh/cli.md",
157
- "read_command": "curl -fsSL https://luxin.sh/skill.md",
164
+ "read_command": "luxin skill --json",
158
165
  "recommended_start_command": "luxin create --guide --prompt PROMPT --json",
159
166
  "model_discovery_command": "luxin models list --available --operation image.generate --json",
160
167
  "model_inspection_command": "luxin models show MODEL_ID --json",
@@ -164,6 +171,7 @@
164
171
  "commands": [
165
172
  "doctor",
166
173
  "trust",
174
+ "skill",
167
175
  "signup --agent --agent-name NAME --runtime RUNTIME",
168
176
  "claim request --contact INBOX",
169
177
  "claim code",
@@ -220,6 +228,15 @@
220
228
  "description": "Return npm provenance, hosted contract hashes, API health, and model availability evidence."
221
229
  }
222
230
  },
231
+ {
232
+ "key": "skill",
233
+ "help": {
234
+ "command": "luxin skill help",
235
+ "usage": "luxin skill --json",
236
+ "docs_url": "https://luxin.sh/cli.md#luxin-skill",
237
+ "description": "Print the full agent guidance bundled with this CLI version, so instructions always match the installed code. No network call, no auth, no spend."
238
+ }
239
+ },
223
240
  {
224
241
  "key": "signup",
225
242
  "help": {
package/llms.txt CHANGED
@@ -3,10 +3,14 @@
3
3
  Status: live preview hosted-product contract
4
4
  Canonical homepage: https://luxin.sh
5
5
  Canonical skill URL: https://luxin.sh/skill.md
6
+ Canonical skill guidance URL: https://luxin.sh/skill-core.md
6
7
  Canonical LLM URL: https://luxin.sh/llms.txt
7
8
  Canonical CLI URL: https://luxin.sh/cli.md
8
9
  Canonical hosted API URL: https://api.luxin.sh
9
10
 
11
+ Skill freshness:
12
+ https://luxin.sh/skill.md is a ~4KB discovery stub carrying identity, the first command, the safety invariants, and metadata.skill_revision. The full guidance is https://luxin.sh/skill-core.md, and `luxin skill --json` prints those same bytes from the installed package, so guidance always matches the running CLI. Export LUXIN_SKILL_REVISION=<metadata.skill_revision of your installed copy>: when it differs from what the CLI ships, `luxin skill` and `create --guide` return data.skill_refresh with installed_revision, current_revision, and refresh_command `npx skills update luxin`, plus one warning line. When it matches or is unset the field is omitted. Refreshing is a command you run; Luxin never rewrites skill files, agent config, or hooks. GET https://api.luxin.sh/healthz publishes the current skill_revision unauthenticated.
13
+
10
14
  Purpose:
11
15
  Luxin is zero-setup creative media for agents: no provider API key, no OAuth, no install to manage, and no per-provider billing account. Agents start with the hosted guide, follow one next command, and use one thin CLI/client and API to generate and edit images, generate video and audio (music, sound), and create 3D assets (image-to-3D mesh, glb), get durable owned media URLs, recover jobs, inspect models, cap spend, pay through credits (browserless agent x402 when returned available; Stripe Checkout human handoff as fallback), and leave feedback. If the guide needs restricted agent signup before first media, it returns that command explicitly.
12
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luxin-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Image generation, image editing, video, audio, and image-to-3D for agents, with no API key to obtain first. The agent signs itself up, starts on free preview credits, and gets hosted media URLs, recoverable jobs, and cost receipts.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -21,6 +21,7 @@
21
21
  "CONTRIBUTING.md",
22
22
  "CODE_OF_CONDUCT.md",
23
23
  "skill.md",
24
+ "skill-core.md",
24
25
  "llms.txt",
25
26
  "cli.md",
26
27
  "commands.json",