@wirecat/cli-core 0.436.0 → 0.438.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/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.438.0 — 26.10.2026
11
+
12
+ ### Security
13
+
14
+ - **A first word refused as a profile no longer reaches the run log.** When the first word could not be a profile
15
+ (`app "runs list"`, two words in one argument) or a profile lock refused it, the failed run's record kept it as
16
+ `profile` while its `argv` showed `[redacted]` — so whatever was typed first, possibly message text or a person's
17
+ name, was stored in the clear. Such a run now records no profile, as a run that names none does; a valid profile
18
+ the settings accepted is recorded as before. The lock's refusal quotes the refused word, so a kept error masks it,
19
+ and a bare first word with no command after it suggests `<tool> <word> account show` only when the word can be a
20
+ profile and the CLI has an `account show` command — that suggestion repeated the word unmasked in the kept error.
21
+
22
+ ## 0.437.0 — 26.10.2026
23
+
24
+ ### Added
25
+
26
+ - **Runs started by OpenClaw or Hermes Agent name that agent, and a Hermes run its session.** OpenClaw's agent sets
27
+ `OPENCLAW_SHELL=exec` in the commands it runs: such a run's `agent` is `openclaw`, with no session, since OpenClaw
28
+ gives none. `tui-local`, what the owner types in OpenClaw's terminal view, stays the owner's. A run started from a
29
+ chat message adds `from_chat: true`; the sender and chat ids OpenClaw passes in `OPENCLAW_CHANNEL_CONTEXT` are never
30
+ read into the record. Hermes was already named `hermes-agent` from `AI_AGENT`; its `HERMES_SESSION_ID` is now the
31
+ session, so a task claim from Hermes is held by `hermes-agent:<session>` as one from Claude Code is by its session.
32
+ A reader of `run.json` that refuses unknown keys must accept the optional `agent.from_chat`.
33
+ - **`validateSkill` accepts OpenClaw's declarations as a mapping at `metadata.openclaw`.** OpenClaw and ClawHub read a
34
+ skill's required programs and its install entry (`requires.bins`, `install: [{ kind: node, package, bins }]`) only
35
+ from an object under that key, so a tool's SKILL.md can now carry them and still pass its own skill check. Every
36
+ other metadata key stays a string, as the Agent Skills specification asks; `openclaw` as a list, a string or null is
37
+ still refused.
38
+
39
+ ### Changed — may break callers
40
+
41
+ - **cli-core needs `@wirecat/cli-base` 0.59.** The peer range moves from `>=0.57.0 <0.58.0` to `>=0.59.0 <0.60.0`. A
42
+ CLI that pins cli-base moves its pin with this release. cli-base 0.59 brings `skill install --for hermes`, which
43
+ gives Hermes Agent the skill, and a `hermes` entry in `skill status`; every CLI on cli-core gets both. cli-base 0.58
44
+ renames the release lock to `wirecat-release-<package>.lock` and drops `leemour` from the shared spelling list.
45
+
10
46
  ## 0.436.0 — 26.10.2026
11
47
 
12
48
  ### Fixed
@@ -7,7 +7,7 @@ export declare const authorOf: (env: NodeJS.ProcessEnv, asOwner?: boolean) => st
7
7
  /**
8
8
  * Who holds a task claim — one agent session, not the agent's name, so two sessions of one agent do not share one
9
9
  * claim (owner, NEED-1119). `--holder`, then `WIRECAT_HOLDER`, then the agent's own session id where it sets one
10
- * (Claude Code, Codex, OpenCode), else the agent's name.
10
+ * (Claude Code, Codex, OpenCode, Hermes), else the agent's name.
11
11
  */
12
12
  export declare const holderOf: (env: NodeJS.ProcessEnv, flag?: string) => string;
13
13
  /**
@@ -9,7 +9,7 @@ export const authorOf = (env, asOwner) => asOwner ? "owner" : (agentOf(env)?.kin
9
9
  /**
10
10
  * Who holds a task claim — one agent session, not the agent's name, so two sessions of one agent do not share one
11
11
  * claim (owner, NEED-1119). `--holder`, then `WIRECAT_HOLDER`, then the agent's own session id where it sets one
12
- * (Claude Code, Codex, OpenCode), else the agent's name.
12
+ * (Claude Code, Codex, OpenCode, Hermes), else the agent's name.
13
13
  */
14
14
  export const holderOf = (env, flag) => {
15
15
  const given = flag?.trim() || env.WIRECAT_HOLDER?.trim();
@@ -1,6 +1,7 @@
1
1
  import type { Command } from "commander";
2
2
  /** How the command line names this profile: always the first word, since none is assumed. */
3
3
  export declare const asFirstWord: (profile: string) => string;
4
+ export declare const isProfileName: (word: string) => boolean;
4
5
  export declare const usableProfileName: (profile: string) => string;
5
6
  /** The top-level commands that never read a profile; every other one is `<tool> <profile> …`. */
6
7
  export declare const PROFILE_FREE: ReadonlySet<string>;
@@ -7,8 +7,9 @@ export const asFirstWord = (profile) => `${profile} `;
7
7
  * somewhere nobody would think to look.
8
8
  */
9
9
  const PLAIN_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10
+ export const isProfileName = (word) => PLAIN_NAME.test(word);
10
11
  export const usableProfileName = (profile) => {
11
- if (!PLAIN_NAME.test(profile)) {
12
+ if (!isProfileName(profile)) {
12
13
  throw new CliError("validation_error", `"${profile}" cannot be a profile name — letters, digits, dot, dash and underscore only, ` +
13
14
  "starting with a letter or a digit");
14
15
  }
@@ -27,7 +27,7 @@ import { showInputFields } from "./input-fields.js";
27
27
  import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
28
28
  import { checkJson, liftJson } from "./json-fields.js";
29
29
  import { PreviewComplete, preview } from "./preview.js";
30
- import { commandWords, liftProfile, sampleCommand, takesProfile, valuedOptions } from "./profile.js";
30
+ import { commandWords, isProfileName, liftProfile, sampleCommand, takesProfile, valuedOptions } from "./profile.js";
31
31
  import { absolutePaths, relayTarget } from "./relay-route.js";
32
32
  import { NoProfileHere, namedProfile, settingsFor, } from "./settings.js";
33
33
  import { assertSharedContracts } from "./shared-contracts.js";
@@ -433,8 +433,11 @@ const runOnce = async (argv, definition, options) => {
433
433
  // A bare word with nothing after it would make commander print help **on stdout**, which breaks
434
434
  // the one contract this program has and says nothing about why.
435
435
  if (profile !== undefined && rest.length === 0) {
436
+ const asProfile = commandAt(program, "account show") && isProfileName(profile)
437
+ ? `, or \`${command} ${profile} account show\` if "${profile}" is your profile`
438
+ : "";
436
439
  const message = `"${profile}" is not a command, so it was read as a profile name — and no command followed it. ` +
437
- `Run \`${command} --help\` for the commands, or \`${command} ${profile} account show\` if "${profile}" is your profile.`;
440
+ `Run \`${command} --help\` for the commands${asProfile}.`;
438
441
  const failure = new CliError("validation_error", message, { param: "profile" });
439
442
  const settled = await settleFailure(failure, { definition, program, rest, argv, profile, options });
440
443
  return report(streams, reporting, failed(definition, program, rest, failure, settled, debug));
@@ -597,7 +600,8 @@ const keepFailure = async (failure, { definition, program, rest, argv, profile,
597
600
  argv,
598
601
  env,
599
602
  }),
600
- profile: settings ? namedProfile(settings) : profile,
603
+ // Settings that did not load never accepted the word, which may be a refused name or somebody's words.
604
+ profile: settings ? namedProfile(settings) : undefined,
601
605
  record: false,
602
606
  keepFailed: settings?.keepFailedRuns ?? !rest.includes("--no-record"),
603
607
  trace: false,
@@ -168,7 +168,7 @@ export const settingsFor = (app, extension = {}) => {
168
168
  if (profile.value === lock)
169
169
  return profile;
170
170
  if (profile.from === "first word" || profile.from === PROFILE) {
171
- throw new CliError("permission_error", `this process is locked to profile ${lock} (${PROFILE_LOCK}) — profile ${profile.value} is refused`);
171
+ throw new CliError("permission_error", `this process is locked to profile ${lock} (${PROFILE_LOCK}) — profile "${profile.value}" is refused`);
172
172
  }
173
173
  return { value: lock, from: PROFILE_LOCK };
174
174
  };
@@ -7059,6 +7059,7 @@ export declare const ENTITIES: {
7059
7059
  readonly agent: v.OptionalSchema<v.StrictObjectSchema<{
7060
7060
  readonly kind: v.StringSchema<undefined>;
7061
7061
  readonly session_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
7062
+ readonly from_chat: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
7062
7063
  }, undefined>, undefined>;
7063
7064
  readonly error: v.OptionalSchema<v.StrictObjectSchema<{
7064
7065
  readonly class: v.StringSchema<undefined>;
@@ -7121,6 +7122,7 @@ export declare const ENTITIES: {
7121
7122
  readonly agent: v.OptionalSchema<v.StrictObjectSchema<{
7122
7123
  readonly kind: v.StringSchema<undefined>;
7123
7124
  readonly session_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
7125
+ readonly from_chat: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
7124
7126
  }, undefined>, undefined>;
7125
7127
  readonly error: v.OptionalSchema<v.StrictObjectSchema<{
7126
7128
  readonly class: v.StringSchema<undefined>;
@@ -7182,6 +7184,7 @@ export declare const ENTITIES: {
7182
7184
  readonly agent: v.OptionalSchema<v.StrictObjectSchema<{
7183
7185
  readonly kind: v.StringSchema<undefined>;
7184
7186
  readonly session_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
7187
+ readonly from_chat: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
7185
7188
  }, undefined>, undefined>;
7186
7189
  readonly error: v.OptionalSchema<v.StrictObjectSchema<{
7187
7190
  readonly class: v.StringSchema<undefined>;
@@ -34,6 +34,7 @@ export declare const recordedRun: v.StrictObjectSchema<{
34
34
  readonly agent: v.OptionalSchema<v.StrictObjectSchema<{
35
35
  readonly kind: v.StringSchema<undefined>;
36
36
  readonly session_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
37
+ readonly from_chat: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
37
38
  }, undefined>, undefined>;
38
39
  readonly error: v.OptionalSchema<v.StrictObjectSchema<{
39
40
  readonly class: v.StringSchema<undefined>;
@@ -96,6 +97,7 @@ export declare const runDetails: v.StrictObjectSchema<{
96
97
  readonly agent: v.OptionalSchema<v.StrictObjectSchema<{
97
98
  readonly kind: v.StringSchema<undefined>;
98
99
  readonly session_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
100
+ readonly from_chat: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
99
101
  }, undefined>, undefined>;
100
102
  readonly error: v.OptionalSchema<v.StrictObjectSchema<{
101
103
  readonly class: v.StringSchema<undefined>;
@@ -157,6 +159,7 @@ export declare const runHit: v.StrictObjectSchema<{
157
159
  readonly agent: v.OptionalSchema<v.StrictObjectSchema<{
158
160
  readonly kind: v.StringSchema<undefined>;
159
161
  readonly session_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
162
+ readonly from_chat: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
160
163
  }, undefined>, undefined>;
161
164
  readonly error: v.OptionalSchema<v.StrictObjectSchema<{
162
165
  readonly class: v.StringSchema<undefined>;
@@ -28,7 +28,7 @@ export const recordedRun = v.strictObject({
28
28
  cli_command: v.optional(text),
29
29
  argv: v.optional(v.array(text)),
30
30
  unknown_command: v.optional(text),
31
- agent: v.optional(v.strictObject({ kind: text, session_id: v.nullable(text) })),
31
+ agent: v.optional(v.strictObject({ kind: text, session_id: v.nullable(text), from_chat: v.optional(v.literal(true)) })),
32
32
  error: v.optional(v.strictObject({
33
33
  class: text,
34
34
  code: v.optional(text),
@@ -5,12 +5,15 @@ export declare const REDACTED = "[redacted]";
5
5
  export interface RunAgent {
6
6
  kind: string;
7
7
  session_id: string | null;
8
+ /** Set, and only `true`, when a chat message started the agent's turn; who sent it is never kept. */
9
+ from_chat?: true;
8
10
  }
9
11
  /**
10
12
  * The agent is read from what each one sets in the processes it starts: Claude Code `CLAUDECODE` and
11
- * `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_THREAD_ID`, OpenCode `OPENCODE` and `OPENCODE_SESSION_ID`, Gemini CLI
12
- * `GEMINI_CLI`, Cursor `CURSOR_AGENT` — the last two give their commands no session id. `AI_AGENT` (Claude Code,
13
- * OpenCode) carries a version, `claude-code_2-1-296_agent`, so only the name before it is kept.
13
+ * `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_THREAD_ID`, OpenCode `OPENCODE` and `OPENCODE_SESSION_ID`, Hermes
14
+ * `AI_AGENT=hermes-agent` and `HERMES_SESSION_ID`, Gemini CLI `GEMINI_CLI`, Cursor `CURSOR_AGENT`, OpenClaw
15
+ * `OPENCLAW_SHELL=exec` — the last three give their commands no session id. `AI_AGENT` (Claude Code, OpenCode, Hermes)
16
+ * carries a version, `claude-code_2-1-296_agent`, so only the name before it is kept.
14
17
  */
15
18
  export declare const agentOf: (env: NodeJS.ProcessEnv) => RunAgent | undefined;
16
19
  export interface MaskedArgv {
@@ -2,9 +2,10 @@ import { isContentKey } from "@wirecat/cli-base/standards";
2
2
  export const REDACTED = "[redacted]";
3
3
  /**
4
4
  * The agent is read from what each one sets in the processes it starts: Claude Code `CLAUDECODE` and
5
- * `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_THREAD_ID`, OpenCode `OPENCODE` and `OPENCODE_SESSION_ID`, Gemini CLI
6
- * `GEMINI_CLI`, Cursor `CURSOR_AGENT` — the last two give their commands no session id. `AI_AGENT` (Claude Code,
7
- * OpenCode) carries a version, `claude-code_2-1-296_agent`, so only the name before it is kept.
5
+ * `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_THREAD_ID`, OpenCode `OPENCODE` and `OPENCODE_SESSION_ID`, Hermes
6
+ * `AI_AGENT=hermes-agent` and `HERMES_SESSION_ID`, Gemini CLI `GEMINI_CLI`, Cursor `CURSOR_AGENT`, OpenClaw
7
+ * `OPENCLAW_SHELL=exec` — the last three give their commands no session id. `AI_AGENT` (Claude Code, OpenCode, Hermes)
8
+ * carries a version, `claude-code_2-1-296_agent`, so only the name before it is kept.
8
9
  */
9
10
  export const agentOf = (env) => {
10
11
  const named = env.AI_AGENT?.trim().split("_")[0];
@@ -13,22 +14,29 @@ export const agentOf = (env) => {
13
14
  (env.CODEX_THREAD_ID || env.CODEX_SESSION_ID || env.CODEX_SANDBOX ? "codex" : undefined) ??
14
15
  (env.GEMINI_CLI ? "gemini-cli" : undefined) ??
15
16
  (env.OPENCODE ? "opencode" : undefined) ??
16
- (env.CURSOR_AGENT ? "cursor" : undefined);
17
+ (env.CURSOR_AGENT ? "cursor" : undefined) ??
18
+ // Only `exec` is the agent's own command: OpenClaw also sets `tui-local` for what the owner types in its TUI.
19
+ (env.OPENCLAW_SHELL?.trim() === "exec" ? "openclaw" : undefined);
17
20
  if (!kind)
18
21
  return undefined;
19
22
  const session = sessionOf(kind, env);
20
- return { kind, session_id: session && SESSION.test(session) ? session : null };
23
+ return {
24
+ kind,
25
+ session_id: session && SESSION.test(session) ? session : null,
26
+ // OpenClaw passes the sender and chat ids of a chat-started run in this variable; only its presence is kept.
27
+ ...(kind === "openclaw" && env.OPENCLAW_CHANNEL_CONTEXT?.trim() ? { from_chat: true } : {}),
28
+ };
21
29
  };
22
30
  const SESSION = /^[A-Za-z0-9._:-]{1,128}$/;
31
+ const SESSION_VARIABLE = {
32
+ "claude-code": "CLAUDE_CODE_SESSION_ID",
33
+ codex: "CODEX_THREAD_ID",
34
+ opencode: "OPENCODE_SESSION_ID",
35
+ "hermes-agent": "HERMES_SESSION_ID",
36
+ };
23
37
  const sessionOf = (kind, env) => {
24
- const value = kind === "claude-code"
25
- ? env.CLAUDE_CODE_SESSION_ID
26
- : kind === "codex"
27
- ? env.CODEX_THREAD_ID
28
- : kind === "opencode"
29
- ? env.OPENCODE_SESSION_ID
30
- : undefined;
31
- return value?.trim() || undefined;
38
+ const variable = SESSION_VARIABLE[kind];
39
+ return (variable === undefined ? undefined : env[variable]?.trim()) || undefined;
32
40
  };
33
41
  const plainWord = (word) => /^[A-Za-z][A-Za-z0-9.-]{0,39}$/.test(word);
34
42
  /**
@@ -1,6 +1,9 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { parseDocument } from "yaml";
4
+ // The Agent Skills specification asks for string values; OpenClaw and ClawHub read their install and requirement
5
+ // declarations only from an object at `metadata.openclaw`, so that one key is a mapping.
6
+ const isMap = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
7
  export const validateSkill = (content, options = {}) => {
5
8
  const problems = [];
6
9
  const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content);
@@ -36,11 +39,9 @@ export const validateSkill = (content, options = {}) => {
36
39
  if (!known.has(key))
37
40
  problems.push(`unsupported frontmatter field: ${key}`);
38
41
  if (fields.metadata !== undefined &&
39
- (!fields.metadata ||
40
- typeof fields.metadata !== "object" ||
41
- Array.isArray(fields.metadata) ||
42
- Object.values(fields.metadata).some((value) => typeof value !== "string")))
43
- problems.push("skill metadata must contain string values");
42
+ (!isMap(fields.metadata) ||
43
+ Object.entries(fields.metadata).some(([key, value]) => key === "openclaw" ? !isMap(value) : typeof value !== "string")))
44
+ problems.push("skill metadata must contain string values, and metadata.openclaw a mapping");
44
45
  if (options.version && fields.metadata?.version !== options.version)
45
46
  problems.push("emitted skill version must match the binary");
46
47
  const body = content.slice(match[0].length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.436.0",
3
+ "version": "0.438.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
@@ -217,7 +217,7 @@
217
217
  "@types/node": "^22.20.5",
218
218
  "@types/pngjs": "6.0.5",
219
219
  "@vitest/coverage-v8": "^5.0.5",
220
- "@wirecat/cli-base": "0.57.0",
220
+ "@wirecat/cli-base": "0.59.0",
221
221
  "@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
222
222
  "commander": "15.0.0",
223
223
  "cspell": "10.4.0",
@@ -237,7 +237,7 @@
237
237
  },
238
238
  "peerDependencies": {
239
239
  "@napi-rs/canvas": ">=0.1.69 <2",
240
- "@wirecat/cli-base": ">=0.57.0 <0.58.0",
240
+ "@wirecat/cli-base": ">=0.59.0 <0.60.0",
241
241
  "commander": ">=15"
242
242
  },
243
243
  "bin": {