Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .claude/skills/use-cmeta-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,15 @@ Parsed by `cmeta/utils/cli.py::parse_cmd`. Rules:
| `--key` | boolean `True` |
| `--key-` | boolean `False` |
| `--no-key` | boolean `False` |
| `--key=value` / `--key value` | string / int (repeats overwrite — last wins) |
| `--key=value` | string / int (repeats overwrite — last wins); `--key value` is the flag `--key` (True) + a positional `value` |
| `--key,=v1,v2,v3` | list `[v1, v2, v3]` (trailing comma on the key requests split) |
| `--key,=` | empty list `[]` |
| `--parent.child=v` | nested dict `{'parent': {'child': v}}` |
| `key=value` (no dashes) | same as `--key=value` |
| `key=value` (no dashes) | same as `--key=value` when the part before `=` looks like a key (a letter or `_`, then letters, digits, `_ . , -`); other text with `=` (`"echo a=b"`, a URL with a query) is positional |
| `-5`, `-1.5` | positional (a negative number is not a flag) |
| `@somefile.yaml` | inline the YAML/JSON file as flags (deep-merged) |
| `@@somefile.yaml` | same, then delete the file |
| `--` | everything after → `unparsed` list |
| `--` | everything after → `unparsed` list, for the commands that read it (others say "takes no arguments after --") |
| bare token | positional → `arg1`, `arg2`, ... |

Examples:
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@
All notable changes to cMeta are documented here, newest first.


## 0.36.0
- **The command line: free text with `=` stays text, a negative number is a value, a control flag takes a word,
and `--` is refused in words.** Four small corrections of the parser, none of which changes a command that
worked: (1) a bare `key=value` is a flag only when the part before the first `=` looks like a key (a letter or
`_`, then letters, digits, `_`, `.`, `,`, `-`) - `cx host run h "echo a=b"`, a URL with a query string or a
Python one-liner with `==` was cut into a parameter named by its first words ("unexpected keyword argument
'echo a'") and is one positional argument now; every key that worked (`name=v`, `a.b=v`, `tags,=a,b`,
`name=John Smith`, `path=C:\x\y`) works as before; (2) a negative number (`-5`, `-1.5`) is a positional
argument, not a flag named `5` (`--delta=-5` was and is a value); (3) the engine's own control flags
(`--quiet`, `--verbose`, `--json`, `--debug`, ...) given a word - `--quiet=yes`, `--json=no` - took the value
converted for the flag before them or failed with "cannot access local variable 'converted_value'" (a typo);
they read yes/no, true/false, on/off, 1/0 now and refuse any other word by name; (4) text after `--`
reaches a command as `unparsed`, and a keyword-style command that has no such parameter says `"<category>
<command>" takes no arguments after "--"` instead of Python's TypeError. The docs said `--key value` was a
form: it never was (`--key value` is the flag `--key` and a positional `value`) and the docs say so now.
Tests: `tests/core_tests/test_cli_parse_table.py` - the table of every token kind, every bool control flag
with every word, the three command styles with text after `--`, and the command line end to end.


## 0.35.2
- **Any lock timeout is waited for as given, and it is the whole wait.** `CMETA_LOCK_TIMEOUT`,
`CMETA_INDEX_LOCK_TIMEOUT` and the cache wait of cmeta-aops (`CMETA_CACHE_WAIT_TIMEOUT`) took `inf`, `nan` or a
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ authors:
website: "https://cTuning.ai/@gfursin"
affiliation: "cTuning Labs"
orcid: "https://orcid.org/0000-0001-7719-1624"
version: "0.35.2"
version: "0.36.0"
date-released: "2026-10-13"
license: Apache-2.0
repository-code: "https://github.com/cTuningLabs/cmeta"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,8 @@ command):
### Flag syntax at a glance

- Booleans: `--flag` (True), `--flag-` (False), `--no-flag` (False).
- Strings: `--key=value` or `--key value` (repeats overwrite — last wins).
- Strings: `--key=value` (repeats overwrite — last wins); `--key value` is the
flag `--key` and a positional `value`.
- Lists: `--key,=v1,v2,v3` (trailing comma on the key).
- Nested dicts: `--parent.child=value` merges into `{'parent': {'child': ...}}`.
- Reusable param files: `@input.yaml` / `@input.json` deep-merges into the
Expand Down
24 changes: 23 additions & 1 deletion cmeta/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,9 +871,31 @@ def access(
self.logger.debug(f'Calling {command_func_name}() @ {filename}:{start_line}-{end_line}')
self.logger.debug(f' with parameters {command_params} ...')

# The text after "--" reaches a command as "unparsed": a keyword-style function without such a
# parameter (and without **kwargs) cannot take it - say so in words, not with Python's TypeError
keyword_style = command_func_name.endswith('_') and not command_func_name.endswith('__')

if keyword_style and 'unparsed' in command_params:
try:
takes_unparsed = any(p.name == 'unparsed' or p.kind == inspect.Parameter.VAR_KEYWORD
for p in inspect.signature(func).parameters.values())
except (TypeError, ValueError):
takes_unparsed = True # no signature to read: the call itself will tell

if not takes_unparsed:
r = utils.names.restore_cmeta_obj(cmeta_ref_parts, key='artifact', fail_on_error = self_fail_on_error)
if self.catch_error(r): return r
category_str = r['obj']

extra_flags_help = category_meta.get('extra_flags_help', '')

return self.error(f'"{category_str} {command}" takes no arguments after "--" (the text after "--" '
f'is passed to the command unparsed, and this command does not read it).\n\n'
f'Rerun with {extra_flags_help}--help to view API usage and options')

try:
command_params['ctx'] = ctx
if command_func_name.endswith('_') and not command_func_name.endswith('__'):
if keyword_style:
result = func(**command_params)
else:
result = func(command_params)
Expand Down
2 changes: 1 addition & 1 deletion cmeta/internal-repo/_cmr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ authors: "[Grigori Fursin](https://cTuning.ai/@gfursin)"
category: repo,f4f792ab40c7498f
copyright: "2025-2026 Grigori Fursin and cTuning Labs. See the cMeta COPYRIGHT and LICENSE files for details."
permanent: True
version: '0.35.2'
version: '0.36.0'
version_date: '20261013'
234 changes: 234 additions & 0 deletions cmeta/internal-repo/category/utils/api/native_host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
"""
Run and register cMeta artifacts as Mozilla native messaging hosts.

cMeta author and developer: (C) 2025-2026 Grigori Fursin

See the cMeta COPYRIGHT and LICENSE files in the project root for details.
"""

import importlib.util
import json
import os
from pathlib import Path
import re
import struct
import subprocess
import sys


MAX_INPUT = 4 * 1024 * 1024
MAX_OUTPUT = 1024 * 1024 - 1
REF_PATTERN = re.compile(r'^[A-Za-z0-9_.,:-]+::[A-Za-z0-9_.,:-]+$')
HOST_PATTERN = re.compile(r'^[a-z0-9_.-]+$')


def resolve(cm, ref):
"""Resolve a host artifact in the active cMeta home, then validate its declaration."""
if not isinstance(ref, str) or not REF_PATTERN.fullmatch(ref):
raise ValueError('Give one cMeta category::artifact reference (UIDs are recommended).')
category, artifact = ref.split('::', 1)
found = cm.access({'category': category, 'command': 'find', 'arg1': artifact,
'con': False, 'quiet': True})
if found.get('return'):
raise RuntimeError(found.get('error') or 'Native host artifact lookup failed.')
matches = found.get('artifacts', [])
if len(matches) != 1:
raise RuntimeError('Expected one native host artifact in the active cMeta home; found ' + str(len(matches)) + '.')
root = Path(matches[0]['path']).resolve()
declaration = json.loads((root / 'native-host.json').read_text(encoding='utf-8'))
name = declaration.get('name')
extensions = declaration.get('allowed_extensions')
entry_name = declaration.get('entry')
checks = declaration.get('checks', [{'command': 'ping'}])
if not isinstance(name, str) or not HOST_PATTERN.fullmatch(name):
raise ValueError('Invalid native host name in native-host.json.')
if not isinstance(extensions, list) or not extensions or not all(isinstance(x, str) and x for x in extensions):
raise ValueError('Invalid allowed_extensions in native-host.json.')
if not isinstance(entry_name, str) or not entry_name.endswith('.py'):
raise ValueError('Invalid Python entry in native-host.json.')
entry = (root / entry_name).resolve()
if root not in entry.parents or not entry.is_file():
raise ValueError('Native host entry must be a Python file inside its artifact.')
if not isinstance(checks, list) or not checks or not all(isinstance(x, dict) for x in checks):
raise ValueError('Invalid checks in native-host.json.')
return {'ref': ref, 'root': root, 'entry': entry, 'name': name,
'description': declaration.get('description', name),
'allowed_extensions': extensions, 'checks': checks}


def locations(name):
"""Keep launchers independent of the repository and selected cMeta home."""
if os.name == 'nt':
base = Path(os.environ.get('LOCALAPPDATA', str(Path.home() / 'AppData/Local'))) / 'cmeta/native-messaging'
return base / (name + '.bat'), base / (name + '.json')
if sys.platform == 'darwin':
launcher = Path.home() / 'Library/Application Support/cmeta/native-messaging' / (name + '.sh')
manifest = Path.home() / 'Library/Application Support/Mozilla/NativeMessagingHosts' / (name + '.json')
return launcher, manifest
data = Path(os.environ.get('XDG_DATA_HOME', str(Path.home() / '.local/share')))
launcher = data / 'cmeta/native-messaging' / (name + '.sh')
manifest = Path.home() / '.mozilla/native-messaging-hosts' / (name + '.json')
return launcher, manifest


def _registered_manifest(name):
if os.name != 'nt':
return None
import winreg
key_name = 'Software\\Mozilla\\NativeMessagingHosts\\' + name
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_name) as key:
return Path(winreg.QueryValueEx(key, '')[0])
except FileNotFoundError:
return None


def _write_registration(name, manifest):
if os.name == 'nt':
import winreg
key_name = 'Software\\Mozilla\\NativeMessagingHosts\\' + name
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, key_name) as key:
winreg.SetValueEx(key, '', 0, winreg.REG_SZ, str(manifest))


def _owned_previous(previous, host):
"""Permit migration only from this same artifact's legacy registration."""
if not previous or not previous.is_file():
return False
try:
data = json.loads(previous.read_text(encoding='utf-8'))
launcher = Path(data['path']).resolve()
current_launcher, _ = locations(host['name'])
same_name = data.get('name') == host['name']
same_extensions = data.get('allowed_extensions') == host['allowed_extensions']
same_artifact = host['root'] in launcher.parents
stable_launcher = launcher == current_launcher.resolve()
return same_name and same_extensions and (same_artifact or stable_launcher)
except (OSError, ValueError, KeyError):
return False


def install(cm, ref, apply=False):
host = resolve(cm, ref)
launcher, manifest = locations(host['name'])
previous = _registered_manifest(host['name']) if os.name == 'nt' else (manifest if manifest.exists() else None)
if previous and previous.resolve() != manifest.resolve() and not _owned_previous(previous, host):
raise RuntimeError('Native host name is registered to a different artifact: ' + str(previous))
if manifest.exists() and not _owned_previous(manifest, host):
raise RuntimeError('Refusing to overwrite a different native host manifest: ' + str(manifest))
expected = {'name': host['name'], 'description': host['description'],
'path': str(launcher), 'type': 'stdio',
'allowed_extensions': host['allowed_extensions']}
# A cRef contains only shell-safe characters. The active cMeta CLI on PATH
# resolves the current home and artifact afresh on every browser request.
args = ' utils native-host-run ' + ref + ' --quiet'
if os.name == 'nt':
content = ('@echo off\r\nsetlocal DisableDelayedExpansion\r\n'
'where.exe cx >nul 2>nul\r\nif errorlevel 1 goto try_cmeta\r\n'
'cx' + args + '\r\nexit /b %errorlevel%\r\n'
':try_cmeta\r\nwhere.exe cmeta >nul 2>nul\r\n'
'if errorlevel 1 goto missing\r\ncmeta' + args + '\r\n'
'exit /b %errorlevel%\r\n:missing\r\n'
'>&2 echo cMeta CLI cx or cmeta is not on PATH.\r\nexit /b 127\r\n')
else:
content = ('#!/bin/sh\n'
'if command -v cx >/dev/null 2>&1; then exec cx' + args + '; fi\n'
'if command -v cmeta >/dev/null 2>&1; then exec cmeta' + args + '; fi\n'
'echo "cMeta CLI cx or cmeta is not on PATH." >&2\nexit 127\n')
print('Active cMeta home:', cm.home_path)
print('Native host artifact:', host['root'])
print('Native host manifest:', manifest)
if not apply:
print('Review only. Add --apply to register the host.')
return {'return': 0, 'manifest': str(manifest), 'applied': False}
launcher.parent.mkdir(parents=True, exist_ok=True)
manifest.parent.mkdir(parents=True, exist_ok=True)
launcher.write_bytes(content.encode('mbcs' if os.name == 'nt' else 'utf-8'))
if os.name != 'nt':
launcher.chmod(0o700)
manifest.write_text(json.dumps(expected, indent=2) + '\n', encoding='utf-8')
_write_registration(host['name'], manifest)
return {'return': 0, 'manifest': str(manifest), 'applied': True}


def _send(command, request, home):
body = json.dumps(request).encode('utf-8')
env = os.environ.copy()
env['CMETA_HOME'] = str(home)
result = subprocess.run(command, input=struct.pack('=I', len(body)) + body,
capture_output=True, timeout=30, env=env,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
if result.returncode or len(result.stdout) < 4:
raise RuntimeError(result.stderr.decode(errors='replace') or 'Native host returned no frame.')
count = struct.unpack('=I', result.stdout[:4])[0]
if count != len(result.stdout) - 4:
raise RuntimeError('Invalid native host framing.')
response = json.loads(result.stdout[4:])
if response.get('return'):
raise RuntimeError(response.get('error') or str(response))
return response


def check(cm, ref):
host = resolve(cm, ref)
launcher, manifest = locations(host['name'])
if not launcher.is_file() or not manifest.is_file():
raise RuntimeError('Native host is not installed. Run native-host-install --apply first.')
registered = _registered_manifest(host['name']) if os.name == 'nt' else manifest
if not registered or registered.resolve() != manifest.resolve():
raise RuntimeError('Native host registration points elsewhere.')
data = json.loads(manifest.read_text(encoding='utf-8'))
if data.get('path') != str(launcher) or data.get('allowed_extensions') != host['allowed_extensions']:
raise RuntimeError('Native host manifest does not match this artifact.')
if os.name == 'nt':
command = subprocess.list2cmdline([os.environ.get('COMSPEC', 'cmd.exe')]) + \
' /d /v:off /s /c ""' + str(launcher) + '""'
else:
command = [str(launcher)]
for request in host['checks']:
_send(command, request, cm.home_path)
print('Native host and backend checks passed:', host['name'])
print('Active cMeta home:', cm.home_path)
return {'return': 0, 'host': host['name']}


def _read_exact(stream, count):
result = bytearray()
while len(result) < count:
chunk = stream.read(count - len(result))
if not chunk:
raise ValueError('Incomplete native message.')
result.extend(chunk)
return bytes(result)


def serve(cm, ref):
"""Preserve a private binary stdout stream before loading any plugin code."""
if os.name == 'nt':
import msvcrt
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
wire = os.fdopen(os.dup(sys.stdout.fileno()), 'wb', buffering=0)
os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
try:
host = resolve(cm, ref)
size = struct.unpack('=I', _read_exact(sys.stdin.buffer, 4))[0]
if size > MAX_INPUT:
raise ValueError('Native message is too large.')
request = json.loads(_read_exact(sys.stdin.buffer, size).decode('utf-8'))
spec = importlib.util.spec_from_file_location('cmeta_native_host_' + host['name'], host['entry'])
if spec is None or spec.loader is None:
raise RuntimeError('Could not load the native host entry.')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
response = module.dispatch(request, cm=cm)
if not isinstance(response, dict):
raise TypeError('Native host dispatch must return a dictionary.')
except Exception as exc:
response = {'return': 1, 'error': str(exc)}
packet = json.dumps(response, ensure_ascii=False).encode('utf-8')
if len(packet) > MAX_OUTPUT:
packet = json.dumps({'return': 1, 'error': 'The result exceeds the native messaging limit.'}).encode('utf-8')
wire.write(struct.pack('=I', len(packet)) + packet)
wire.flush()
return {'return': 0}
21 changes: 21 additions & 0 deletions cmeta/internal-repo/category/utils/api/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ def __init__(
"""
super().__init__(*args, module_file_path = __file__, **kwargs)

def native_host_install_(self, ctx, arg1=None, apply=False):
"""Register a native messaging host declared by a cMeta artifact."""
from . import native_host
try:
return native_host.install(self.cm, arg1, apply=apply)
except Exception as exc:
return self.cm.error(str(exc))

def native_host_check_(self, ctx, arg1=None):
"""Check native messaging transport and the artifact's declared requests."""
from . import native_host
try:
return native_host.check(self.cm, arg1)
except Exception as exc:
return self.cm.error(str(exc))

def native_host_run_(self, ctx, arg1=None):
"""Serve one framed native message using cx's active cMeta home."""
from . import native_host
return native_host.serve(self.cm, arg1)

############################################################
def test(
self,
Expand Down
Loading
Loading