chore: initialize K1 connector pre-production scaffold

This commit is contained in:
DCCONSTRUCTIONS 2026-07-15 15:05:18 +03:00
commit 4c401bc59b
23 changed files with 4232 additions and 0 deletions

12
.gitattributes vendored Normal file
View File

@ -0,0 +1,12 @@
* text=auto eol=lf
*.md text diff=markdown
*.json text
*.jsonl text
*.toml text
*.pcap binary
*.pcapng binary
*.snoop binary
*.xbin binary
*.las binary
*.lcc binary

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
.DS_Store
.idea/
.vscode/
# Project-local Python environment and caches
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/
# Local configuration and secrets
.env
.env.*
!.env.example
config/local.toml
private/
# Real laboratory captures and decoded artifacts are sensitive and large.
captures/
sessions/
artifacts/raw/
artifacts/decoded/
*.pcap
*.pcapng
*.snoop
*.xbin
*.las
*.lcc
*.ply
*.pcd
*.npz
*.raw
# Small synthetic/redacted fixtures under tests/fixtures are allowed.
!tests/fixtures/**/*.pcap
!tests/fixtures/**/*.pcapng

2
.python-version Normal file
View File

@ -0,0 +1,2 @@
3.12

39
AGENTS.md Normal file
View File

@ -0,0 +1,39 @@
# Repository operating rules
This repository investigates an owner-controlled XGRIDS/LixelKity K1 as a
black-box sensor. Preserve the device, the Mac, the working LAN, and raw
evidence.
## Non-negotiable safety boundaries
- Default to passive or read-only operations.
- Do not send BLE writes until the exact service, characteristic, framing,
payload semantics, rollback, and expected state transition are documented.
- BLE notification subscription may perform the standard temporary CCCD write;
disclose it explicitly and do not conflate it with provisioning writes.
- Do not fuzz, brute-force, upload firmware, delete device files, guess SSH/ADB
credentials, or probe unrelated LAN devices.
- Network probes must target the confirmed K1 IP. A full home-subnet scan is
not a default operation.
- Never place a Wi-Fi password in CLI arguments, logs, manifests, source, test
fixtures, or Git history.
- Do not install Python packages globally. Use the repository-local `.venv`
managed by `uv`.
- Do not install or alter Homebrew/system components unless the user explicitly
authorizes that concrete change.
## Evidence rules
- Reference inputs under `docs/reference/` are immutable; write corrections in
audits or ADRs.
- Every real experiment gets a session ID, UTC and monotonic timestamps, a
redacted manifest, operator notes, and SHA-256 hashes for raw artifacts.
- Real PCAP, K1 projects, logs, images, point clouds, credentials, serials, and
router client lists stay out of normal Git. Git LFS solves size, not secrecy.
- Synthetic or explicitly redacted fixtures may be committed under
`tests/fixtures/`.
## Implementation order
Follow the gates in `docs/01_IMPLEMENTATION_PLAN.md`. Do not build heavy
decoders before BLE/Wi-Fi/data-session evidence exists.

81
README.md Normal file
View File

@ -0,0 +1,81 @@
# NDC XGRIDS K1 Connector
Pre-production research project for connecting an owner-controlled
XGRIDS/LixelKity K1 to a Mac without LixelGO, firmware changes, device opening,
or speculative writes.
Current status: repository scaffold and implementation plan. No command in the
repository currently writes to the K1, changes the router, or installs system
packages.
## Available stand
- one XGRIDS/LixelKity K1;
- one Apple Silicon MacBook running macOS;
- one ordinary TP-Link Deco/mesh network used by other devices;
- no LixelGO, phone, Linux host, dedicated AP, OpenWrt, or vendor SDK.
The ordinary router is sufficient for the first gates. We first observe the
existing LAN without changing it. A Guest/IoT SSID is optional and may be
counterproductive if Deco isolates clients; Mac and K1 must ultimately be able
to reach each other.
## What is actually being proved
The project has three independent gates:
1. K1 is operational and can record a project autonomously.
2. Mac can discover and inspect the K1 BLE/GATT surface safely.
3. Without LixelGO, K1 can be associated with Wi-Fi and a proprietary data
session can be opened.
Only after gate 3 do point-cloud, pose, status, and camera stream decoders become
meaningful implementation work. Local `map.las` and `poses.csv` prove internal
capabilities, not an externally accessible network format.
## Local environment
The project uses Python 3.12 in a repository-local `.venv` managed by `uv`.
This does not install Python packages globally and does not modify neighboring
repositories.
```bash
cd /Users/dcconstructions/Downloads/mnt/NODEDC/NDC_xgrids-k1-connector
uv sync --group dev
uv run k1link doctor
uv run pytest
```
`doctor` is intentionally non-invasive. It checks the local Python environment
and reports external tools; it does not request Bluetooth permission, scan the
LAN, touch the K1, alter Homebrew, or change capture permissions.
## Documentation
- [Technical audit](docs/00_TECHNICAL_AUDIT.md)
- [Implementation gates](docs/01_IMPLEMENTATION_PLAN.md)
- [First lab runbook](docs/02_FIRST_LAB_RUNBOOK.md)
- [Artifact and secret policy](docs/03_ARTIFACT_POLICY.md)
- [Session manifest schema](schemas/session-manifest.schema.json)
- [Reference input provenance](docs/reference/README.md)
The two supplied source documents are retained unchanged under
`docs/reference/`. Corrections and decisions are recorded separately so their
provenance remains clear.
## Safety boundary
Allowed initial work is passive discovery, standard device-information reads,
controlled notification listening, autonomous button operation, targeted
capture of traffic to or from the confirmed K1 address, and offline analysis of
owned artifacts.
BLE writes, provisioning, application-session packets, active service probes,
and router configuration changes require evidence and an explicit reviewed
step. Random writes, fuzzing, brute force, firmware operations, destructive file
access, and credential guessing are out of scope.
Real captures, projects, router metadata, serials, credentials, maps, images,
and logs are ignored by normal Git. Redacted manifests and SHA-256 inventories
are committed; encrypted artifact storage will be selected only when real data
exists.

9
artifacts/README.md Normal file
View File

@ -0,0 +1,9 @@
# Laboratory artifacts
Only redacted manifests and SHA-256 inventories belong in ordinary Git.
Real captures, K1 project copies, logs, point clouds, images, router client
lists, and decoded output must stay in ignored paths such as `artifacts/raw/`
or `sessions/`. If long-term versioned storage becomes necessary, choose and
document an encrypted artifact store. Git LFS alone is not encryption.

View File

@ -0,0 +1 @@

158
docs/00_TECHNICAL_AUDIT.md Normal file
View File

@ -0,0 +1,158 @@
# Technical audit
Status: pre-production planning, 2026-07-15.
## Executive finding
The project is feasible as a disciplined black-box investigation, but the
existence of a complete K1-to-Mac realtime link is not yet established. The
first hard problem is not decoding points or images. It is bootstrapping the K1
from BLE availability into Wi-Fi association and then opening the proprietary
application data session without LixelGO.
The supplied Bible is useful as an OSINT dossier. It is not an executable plan
for the actual stand because many experiments assume a phone and LixelGO. The
implementation must proceed through explicit gates and stop before speculative
writes.
## Evidence classes
### Version-scoped facts to verify on the physical unit
- K1 has LiDAR, panoramic cameras, onboard compute, BLE and Wi-Fi.
- Official workflows use BLE before Wi-Fi network/data connection.
- Autonomous scan start/stop by the physical button is documented.
- A scan project may contain local `map.las`, `poses.csv`, `project.json`, logs,
preview/model and raw sensor artifacts depending on firmware and settings.
- macOS exposes a CoreBluetooth device UUID rather than a portable hardware MAC.
- A normal switched/mesh LAN does not mirror arbitrary client-to-client unicast
to the Mac, but the Mac can capture its own future K1 session completely.
Each of these remains scoped to the actual firmware/hardware state observed in
the lab. The physical unit is authoritative.
### Strong hypotheses
- BLE is the bootstrap/control plane and Wi-Fi is the likely high-rate data
plane.
- The user-provided SSID and PSK are transported inside a vendor-defined GATT
protocol.
- The application may need a second token, certificate, handshake, or stream
subscription after ordinary Wi-Fi association.
- The externally exposed point cloud is more likely a processed/downsampled
preview than raw LiDAR packets.
XGRIDS provides stronger product evidence for an external point-cloud path than
the local LAS alone: current LixelStudio materials describe K1 remote control
and realtime point-cloud streaming over USB/Wi-Fi. LixelStudio is Windows-only
and this does not disclose or guarantee access to the protocol from macOS. It
does, however, justify keeping realtime point cloud as the primary stream target.
No comparable official evidence proves an exportable raw panorama/camera stream.
### Unsupported until measured
- K1 remembers an existing Wi-Fi profile.
- K1 exposes its own access point.
- Provisioning fields are plain UTF-8, JSON, TLV, CBOR, or protobuf.
- A writable GATT characteristic can be used without bonding or an app token.
- Local `map.las` layout resembles the network stream.
- Live pose or camera frames leave the device.
- A point stream contains simple float32 XYZ tuples.
- Network payloads are unencrypted.
## Major corrections to the source plan
1. Experiments requiring LixelGO are removed from the active critical path.
There will be no app-session capture, provisioning diff, app-start comparison,
or temporary broker mode.
2. The first go/no-go is device activation and autonomous operation. An
unactivated unit may be a hard blocker before BLE provisioning is relevant.
3. Provisioning is not implemented merely because a characteristic is writable.
We need target UUID, framing, field encoding, ordering, integrity/auth fields,
connect/commit semantics, status response, and rollback.
4. Wi-Fi association and application data session are modeled as separate
states. Obtaining an IP is not success for the realtime connector.
5. `tcpdump` does not create a stream. A useful capture requires either K1 to
initiate traffic or Mac to become the authenticated/subscribed endpoint.
6. Full port/version/UDP scans are active operations, not passive safe defaults.
Discovery begins with power on/off diffs, router client list, ARP and
broadcast/multicast observations, then targets only the confirmed K1 IP.
7. Heavy packages are deferred. OpenCV, Open3D, pandas, Scapy, PyShark and NumPy
are not prerequisites for proving bootstrap connectivity.
8. Wi-Fi passwords must never be CLI arguments. Future provisioning will use a
hidden interactive prompt or macOS Keychain and must redact payloads.
## Router audit
The existing TP-Link Deco/mesh router is adequate for the initial work. No
router setting needs to change before the first observations.
We need only DHCP, a shared reachable network, and ideally a way to view the
client list. Band steering and a single combined SSID are acceptable. A separate
Guest/IoT SSID is optional; on some Deco modes it isolates clients, which would
prevent Mac-to-K1 traffic. If a separate SSID is later used, peer-to-peer reachability
must be tested before provisioning K1 onto it.
The main-LAN fallback is acceptable for a narrow, target-filtered experiment.
It must not include a broad scan or unfiltered capture of household traffic.
## Environment audit
Observed host baseline:
- Apple Silicon (`arm64`);
- macOS 26.5.1;
- `uv` and Homebrew present;
- system Python is 3.13.5, while the project requires 3.12;
- `tcpdump` and `ffmpeg` present;
- `tshark` and `nmap` not currently found.
- the current default route is a VPN tunnel (`utun`), while Wi-Fi is `en0`.
The project therefore pins Python 3.12 via `.python-version`; `uv` creates a
local `.venv`. Nothing in setup installs global Python packages. Missing external
network-analysis tools are reported by `k1link doctor` and are not installed
until their gate requires them and the change is explicitly accepted.
Future network commands must determine the route for the confirmed K1 IP;
neither the default route nor `en0` may be hardcoded.
## Primary blockers
| Gate | Blocking condition | Meaning |
|---|---|---|
| Device | fault, activation lock, no autonomous project | Stop before networking |
| BLE | no advertisement or only inaccessible/authenticated surface | Investigate state/permissions before writes |
| Wi-Fi | no remembered network/AP and opaque custom GATT | Provisioning research is the hard block |
| Data session | IP exists but service requires unknown token/handshake | Association succeeded; connector has not |
| Streams | session opens but no useful external stream | Realtime goal may be unsupported |
| Camera | point cloud works but frames never appear | Camera branch may be unsupported; not an MVP failure |
## Implementation consequence
The correct order is:
```text
device baseline
-> autonomous project + USB metadata
-> passive BLE surface
-> remembered Wi-Fi / device AP check
-> evidence-led provisioning gate
-> application-session gate
-> targeted capture
-> flow classification
-> point cloud / pose / status / optional camera decoders
```
## Verified primary references
- [XGRIDS K1 firmware and release notes](https://www.xgrids.com/intl/support/download?page=K1)
- [XGRIDS LixelStudio](https://www.xgrids.com/intl/lixelstudio)
- [XGRIDS LixelStudio releases](https://www.xgrids.com/intl/support/download?page=LixelStudio)
- [XGRIDS device activation and connection](https://docs.xgrids.com/en-us/02-lingguang-k/01-lingguang-k1/v2.4.0/03-device-activation-and-connection.html)
- [Apple packet-trace guidance](https://developer.apple.com/documentation/network/recording-a-packet-trace)
- [Bleak macOS backend](https://bleak.readthedocs.io/en/latest/backends/macos.html)
- [TP-Link Deco Guest Network behavior](https://www.tp-link.com/ca/support/faq/1460/)
- [TP-Link Deco IoT Network](https://www.tp-link.com/us/support/faq/4420/)
- [uv project environments](https://docs.astral.sh/uv/guides/projects/)
Reference-document statements not independently repeated here remain inputs or
hypotheses, not verified conclusions of this audit.

View File

@ -0,0 +1,188 @@
# Implementation plan
This plan supersedes the app-dependent experiment order in the reference Bible.
Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result.
## Stage 0 — repository and host baseline
Deliverables:
- local Python 3.12 `.venv` and lock-file;
- `k1link doctor` with no side effects;
- immutable reference documents and checksums;
- safety policy, artifact policy, session manifest schema;
- no Homebrew changes and no K1 interaction.
Acceptance:
- `uv sync --group dev` succeeds;
- `uv run k1link doctor` identifies a local `.venv`;
- tests, Ruff and mypy pass;
- repository contains no secrets or real captures.
## Gate 1 — physical and USB baseline
Code needed: none initially.
Actions:
1. Record masked device identity, LED boot sequence, battery and visible state.
2. Determine whether the unit is activated; solid-yellow or an activation prompt
is treated as a distinct blocker.
3. Run one short autonomous button scan: start, keep still for at least 20 seconds,
move through a simple scene, stop.
4. Enter USB mode using the documented physical shortcut if available.
5. Copy a project tree and selected metadata/logs into an ignored session path.
6. Hash every copied file and create a redacted manifest.
GO: the unit boots normally and records a usable autonomous project.
PAUSE: USB access is unavailable but the scan succeeds; continue BLE while
documenting the missing ground truth.
STOP: serious fault, overheating, activation lock, or destructive/ambiguous
device state.
## Stage 1 — BLE read-only toolkit
Minimal dependencies: `bleak` plus the existing CLI stack. Add them only in the
repository-local environment.
Commands:
```text
k1link ble scan
k1link ble gatt-dump
k1link ble listen
```
Requirements:
- preserve advertisement snapshots, manufacturer/service data and RSSI;
- identify by observed name + macOS UUID + advertisement fingerprint;
- inspect standby, scanning and USB states separately;
- start with standard Device Information/Battery services;
- custom characteristic reads are opt-in because proprietary reads may have
side effects;
- notifications are subscribed one characteristic at a time and logged to
timestamped JSONL;
- no arbitrary characteristic or provisioning writes in this stage. Enabling a
notification normally updates the standard CCCD descriptor; this temporary,
reversible protocol write is disclosed and handled separately from device
configuration.
GO: reproducible K1 advertisement and useful GATT map.
PAUSE: permission or pairing/authentication is required.
## Gate 2 — existing network behavior
Do not change Deco settings first.
Actions:
1. Save the Mac interface and route table.
2. Observe router client list and Mac ARP/neighbor state with K1 off.
3. Repeat after K1 boot, during standby, scanning and after one power cycle.
4. Check for new Wi-Fi SSIDs exposed by K1 without attempting to join unknown
networks automatically.
5. Confirm any candidate by disappearance/reappearance with K1 power state.
GO-A: K1 joins a remembered LAN. Provisioning leaves the immediate critical
path and work moves to the application-session gate.
GO-B: K1 exposes its own AP. Document it as a separate topology and determine
whether Mac can join without modifying router configuration.
BLOCKED: neither association nor AP exists; provisioning becomes Stage 2.
## Stage 2 — no-app provisioning research
This stage answers whether SSID/PSK can be sent safely from Mac. A generic BLE
transport does not define the vendor payload.
Evidence sources allowed before a write:
- GATT UUIDs, properties and descriptors;
- standard service identification;
- read/notify state transitions correlated with network state;
- owned K1 project logs and metadata;
- public official firmware/documentation static evidence if separately reviewed;
- deterministic structure inferred without sending device mutations.
Before enabling any write profile, document:
- exact service/characteristic and required security mode;
- packet framing, encoding, ordering and length rules;
- SSID/PSK redaction and secure input path;
- checksum, nonce, token, certificate or signature fields;
- commit/connect command and status response;
- timeout, failure state, power-cycle behavior and rollback;
- one explicit confirmation flag in addition to a named reviewed profile.
GO: a deterministic provisioning profile exists with an explained expected
state transition and safe rollback.
HARD BLOCK: K1 knows no network, exposes only opaque vendor GATT, and no evidence
source reveals the protocol. Random writes are not an alternative.
## Stage 3 — application session and targeted capture
Prerequisite: confirmed K1 IP or direct AP topology.
Implementation order:
1. Passive ARP/mDNS/SSDP and K1-filtered `tcpdump` baseline.
2. Small targeted TCP connect check against the confirmed IP.
3. Service detection only where the prior step supplies evidence.
4. Parse K1 logs for daemon names, ports and protocol strings.
5. Add protocol-specific connection attempts only for demonstrated endpoints.
6. Model `WIFI_ASSOCIATED_BUT_NO_DATA_SESSION` explicitly.
`k1link net capture` must show the exact interface/filter, request privilege in
the foreground, handle Ctrl+C, and never configure persistent `sudo` or ChmodBPF.
GO: Mac becomes a K1 endpoint or receives a reproducible scan-correlated flow.
BLOCKED: application session requires an unavailable mutual-auth token,
certificate or undocumented handshake.
## Stage 4 — session artifacts and flow analysis
Implement only after real traffic exists:
- versioned session manifest;
- UTC and monotonic timeline events;
- SHA-256 inventory;
- TCP reassembly and UDP flow grouping;
- packet/byte rates, length histograms, entropy and first-byte fingerprints;
- idle versus scanning correlation;
- negative-control flows to reduce false positives.
GO: repeatable flows can be separated and ranked without losing raw evidence.
## Stage 5 — decoders by evidence priority
Priority:
1. device/scan status;
2. point-cloud preview;
3. pose/trajectory;
4. camera/panorama/video.
Add dependencies per decoder extra, not to the base environment. Point-cloud
and video signature probes operate on reassembled flows, not isolated packet
payloads. A candidate becomes a decoder only after a controlled physical test
confirms it.
MVP GO: K1-to-Mac scan-correlated data can be captured reliably and at least one
useful stream is decoded or structurally identified.
## Deferred work
- ROS2/MCAP bridge;
- onboard computer and drone integration;
- automated scan-button electronics;
- OpenWrt/monitor-mode infrastructure;
- firmware or internal-Linux analysis;
- camera branch if no external frame stream is evidenced.

View File

@ -0,0 +1,134 @@
# First lab runbook
Purpose: gather decisive evidence using only K1, the current MacBook and the
existing TP-Link Deco/mesh network. This runbook contains no BLE writes, router
changes, broad LAN scans or system package installation.
## Before powering K1
- Place K1 on a stable, ventilated surface.
- Prepare its normal battery/charger and a known data-capable USB cable.
- Ensure enough Mac disk space for one copied project and short captures.
- Do not expose the Wi-Fi password in notes, terminal arguments or screenshots.
- If the router model becomes visible in the Deco UI, record only the model and
operating mode; do not change settings yet.
Create an ignored session directory outside ordinary Git, for example:
```text
sessions/20260715T120000Z_gate1_baseline/
```
The operator notes should use masked serials and relative timestamps.
## Experiment A — boot and activation state
1. Start a phone/Mac video focused on the LED if convenient.
2. Power K1 on normally.
3. Record LED colors and durations until stable.
4. Record whether the final state is normal standby, activation warning or fault.
5. Stop immediately on serious fault, unusual heat, smell, battery problem or
undocumented state.
Expected output: `notes.md`, optional local video, and a redacted manifest.
## Experiment B — autonomous project
1. From normal standby, start scanning with the documented double-click.
2. Keep K1 completely still for at least 20 seconds.
3. Move slowly through a simple scene for about one minute.
4. Stop scanning with the documented double-click and wait for stable standby.
5. Enter USB mode using the physical shortcut only if its state is understood.
6. Copy the project tree, `project.json`, `poses.csv`, `map.las` metadata and logs
when present. Raw project data remains ignored and local.
7. Generate a SHA-256 inventory; commit only a redacted report later.
Decision:
- GO if a valid autonomous project exists.
- PAUSE USB analysis if recording works but USB is unavailable.
- STOP on activation lock or device fault.
## Experiment C — passive BLE surface
Prerequisite: Stage 1 BLE commands implemented and reviewed.
1. Run a 30-second scan in standby.
2. Save advertisement and manufacturer/service data.
3. Connect for service discovery without writes.
4. Read standard device-information/battery characteristics first.
5. Repeat advertisement/GATT observation during scanning and USB mode.
6. Subscribe to an individually selected notify/indicate characteristic only
after its properties are recorded.
Decision:
- GO if K1 has a reproducible BLE identity and observable surface.
- PAUSE if macOS asks for pairing/authentication that cannot be completed.
## Experiment D — current network, no router changes
1. Capture the Mac route/interface and its current neighbor table with K1 off.
2. Note the Deco client list if accessible.
3. Boot K1 and repeat in standby.
4. Repeat during a button scan.
5. Power-cycle K1 and confirm whether any candidate disappears/reappears.
6. Check visible SSIDs for a K1-associated AP.
Do not infer identity from one unknown LAN client. Require power-state
correlation or an independent device fingerprint.
Decision:
- remembered-LAN GO: confirmed K1 address appears;
- device-AP GO: a repeatable K1 SSID appears;
- provisioning BLOCKED: neither appears and no known GATT provisioning profile
exists.
## About Guest/IoT networks on Deco
Do not create one during this first run. Depending on Deco model and Router/AP
mode, Guest/IoT networks may isolate clients from the main LAN or from each
other. A later dedicated SSID is useful only if both K1 and Mac can communicate
and its password can be entered through a known provisioning profile.
## Minimum lab report
```markdown
# K1 Lab Report 001
## Host
macOS:
Mac model/architecture:
Router model/mode (if known):
## Device
Model:
Serial suffix only:
Activation/LED state:
Firmware evidence:
## Autonomous scan
Start/stop behavior:
Project created:
USB accessible:
Files observed:
## BLE
Advertised name:
macOS UUID: [redacted]
Service fingerprint:
Pairing/auth prompts:
## Network
Remembered LAN candidate:
Device AP candidate:
Confirmed K1 IP: [redacted]
## Decision
Current gate:
GO / PAUSE / BLOCKED:
Evidence:
Next smallest experiment:
```

View File

@ -0,0 +1,60 @@
# Artifact and secret policy
The experiment should be reproducible from Git without placing household
credentials, spatial maps or device secrets into normal Git history.
## Sensitive by default
- Wi-Fi SSID and PSK;
- full K1 serial, BLE UUID, MAC/IP addresses and router client lists;
- provisioning payloads, tokens, nonces, certificates and session keys;
- PCAP/PCAPNG, Bluetooth logs and extracted flows;
- K1 logs, `project.json`, `.xbin`, LAS/point clouds and trajectories;
- camera frames, previews, panoramas and mapped interiors;
- command lines or shell history containing a credential.
Private Git is still durable replicated storage. Git LFS changes storage and
size behavior; it does not provide confidentiality.
## Commit to normal Git
- source, tests and lock-file;
- immutable reference documents and their hashes;
- schemas, runbooks, ADRs and redacted reports;
- synthetic or explicitly redacted fixtures;
- redacted manifests and SHA-256 inventories;
- commands needed to reproduce analysis from a locally supplied artifact.
## Keep outside normal Git
- `.venv`, local config and Keychain references;
- actual credentials;
- real captures and decoded output;
- complete K1 project copies;
- unredacted device and router metadata.
## Session integrity
Every real session should record:
- schema version and unique session ID;
- tool Git commit;
- UTC start/end and Mac monotonic start/end;
- masked device/host/network identities;
- physical state timeline;
- exact capture filter and interface;
- artifact path, size and SHA-256;
- redaction status and storage location classification;
- operator notes and final GO/PAUSE/BLOCKED decision.
Raw artifacts are never silently deleted by code. That does not imply infinite
retention: capacity, encryption, backup and deliberate deletion policy are an
operator decision outside the capture command.
## Future provisioning secret input
The Wi-Fi password must not be accepted as a normal CLI argument. A reviewed
provisioner should use a hidden prompt, stdin/file descriptor with strict
permissions, or macOS Keychain. Logs and manifests record only that a credential
was supplied, never its value or unredacted payload bytes.

View File

@ -0,0 +1,32 @@
# ADR 0001: Treat LixelGO as unavailable
Status: accepted.
## Context
The actual stand consists of one K1, one MacBook and one ordinary TP-Link
Deco/mesh router. There is no phone, LixelGO reference session, Linux host,
dedicated capture router or public SDK.
The supplied design documents include several app-dependent experiments and
assume that provisioning payloads can be recovered by comparing LixelGO BLE
writes. That evidence source does not exist.
## Decision
- LixelGO is not a temporary dependency, broker or test oracle.
- App-dependent experiments remain only as historical reference.
- The active critical path is physical baseline, passive BLE, remembered Wi-Fi
or device AP, evidence-led provisioning, application-session discovery, then
stream capture and decoding.
- A fully opaque provisioning protocol with no evidence source is reported as a
hard black-box blocker rather than approached with random writes.
- The existing router is used without changes for the first observation gate.
## Consequences
The project may reach a legitimate BLOCKED result before receiving live data.
This is still a successful pre-production conclusion because it identifies the
exact missing interface and prevents premature decoder work or risky device
mutation.

View File

@ -0,0 +1,220 @@
# Codex prompt: k1-mac-link
Ты работаешь над проектом `k1-mac-link`.
## Контекст
Есть официальный XGRIDS / LixelKity K1, MacBook, Wi-Fi router. Linux как отдельного стенда нет. Цель - использовать K1 как black-box realtime computer vision sensor без модификации firmware и без вскрытия на первом этапе.
Нужны realtime/cached данные:
- point cloud / preview cloud;
- pose / trajectory / odometry-like stream, если есть;
- camera / panoramic preview / RGB stream, если есть;
- device status;
- raw packets для reverse engineering.
Не нужно в runtime:
- считать LAS;
- считать LCC;
- считать Gaussian splats;
- менять firmware;
- делать brute force;
- слать random BLE writes.
## Главная архитектура
Официальные мануалы K1 показывают такую модель:
- BLE используется для discovery/provisioning;
- SSID/password - это реквизиты нашей Wi-Fi сети;
- после подключения к Wi-Fi есть отдельный `data connection status`;
- K1 умеет автономно стартовать scanning double-click кнопкой;
- K1 пишет `map.las` как realtime point cloud output, но downsampled;
- K1 пишет `poses.csv` как trajectory recorded during scanning;
- K1 содержит 2 panoramic cameras и 360 deg LiDAR;
- live camera stream наружу не доказан, его надо искать экспериментально.
## Создай проект
Python 3.12, macOS-first, Typer CLI.
Структура:
```text
k1-mac-link/
README.md
pyproject.toml
docs/
src/k1link/
cli.py
config.py
logging.py
artifacts.py
ble/
scanner.py
gatt_dump.py
listener.py
provisioner.py
net/
discover.py
ports.py
capture.py
mdns.py
tshark.py
analysis/
pcap_summary.py
flow_classifier.py
payload_extract.py
payload_fingerprint.py
protocol_diff.py
pointcloud_probe.py
video_probe.py
timeline.py
decode/
pointcloud.py
pose.py
video.py
status.py
cache/
writer.py
manifest.py
raw_stream.py
export/
ply.py
pcd.py
npz.py
frames.py
tests/
```
## Dependencies
Use:
```text
bleak
typer
rich
pydantic
pydantic-settings
loguru
pyshark
scapy
construct
numpy
pandas
opencv-python
open3d
pillow
zeroconf
ifaddr
psutil
pytest
pytest-asyncio
ruff
mypy
```
## CLI commands
Implement:
```bash
k1link doctor
k1link ble scan --duration 30 --out captures/ble_scan.json
k1link ble gatt-dump --device <macos_uuid> --out captures/gatt_dump.json
k1link ble listen --device <macos_uuid> --duration 120 --out captures/ble_notifications.jsonl
k1link net discover --subnet 192.168.1.0/24 --out captures/net_discover.json
k1link net ports --ip <K1_IP> --out captures/ports.json
k1link net capture --iface en0 --host <K1_IP> --out captures/session.pcap
k1link analyze summarize --pcap captures/session.pcap --out captures/session_summary.json
k1link analyze extract-flows --pcap captures/session.pcap --out-dir captures/flows
k1link analyze fingerprint --flows captures/flows --out captures/fingerprint.json
k1link analyze pointcloud-probe --flows captures/flows --out-dir captures/pointcloud_candidates
k1link analyze video-probe --flows captures/flows --out-dir captures/video_candidates
```
## Safety rules
Allowed by default:
- BLE scan;
- GATT discovery;
- read characteristics;
- notifications;
- network discovery;
- port scanning of owned device;
- tcpdump capture;
- pcap analysis.
Forbidden by default:
- BLE write;
- random fuzzing;
- brute force;
- firmware upload;
- deleting/modifying K1 files;
- SSH/ADB login attempts.
Any BLE write must require:
```bash
--i-understand-this-writes-to-device
```
Provisioner stays NotImplemented until a known payload profile is manually added.
## Outputs
Every session creates:
```text
session_manifest.json
notes.md
captures/*.pcap
captures/*.json
flows/*.bin
flows/*.jsonl
analysis/*.json
decoded/pointcloud/*
decoded/video/*
decoded/pose/*
```
Raw captures are never deleted.
## Implement pointcloud probe
Try:
- float32 little-endian XYZ;
- offsets 0..128;
- strides 12, 16, 20, 24, 28, 32, 40, 48;
- scaled int16/int32 fallback;
- score finite ratio, coordinate range, variance, temporal continuity;
- export PLY/NPZ for top candidates.
## Implement video probe
Detect/extract:
- JPEG FF D8 FF ... FF D9;
- PNG signature;
- H264/H265 Annex B;
- MP4 ftyp/moof/mdat;
- RTSP/RTP;
- MJPEG;
- WebSocket binary large frames.
Try ffmpeg/OpenCV extraction where possible.
## README must include
- macOS setup;
- Wireshark ChmodBPF note;
- Bluetooth permission note;
- Bleak macOS UUID caveat;
- K1 lab network setup;
- experiment flow: idle capture, button scan capture, app scan capture, camera perturbation, point cloud perturbation.

15
docs/reference/README.md Normal file
View File

@ -0,0 +1,15 @@
# Reference inputs
These files are immutable copies of the user-supplied pre-production material.
They are retained as inputs, not treated as verified specifications.
| File | SHA-256 | Role |
|---|---|---|
| `CODEX_K1_MAC_LINK_PREPROD_PROMPT.md` | `f24537119bd76f76d1914ca7f34a6736d8c5d3b6d19ead6d0ae8a0e5966c9770` | Requested project shape and CLI scope |
| `XGRIDS_Lixel_K1_PreProduction_Bible.md` | `22ea9d0dc6f5a623ba26d6f08108a905170db08112e0531a1d80bddd6101689c` | OSINT dossier, hypotheses, experiments and long-form design |
Important contextual correction: the actual stand has no LixelGO and no mobile
device. All experiments in the source documents that require an app or a
reference app session are unavailable. The active plan is therefore defined by
`docs/01_IMPLEMENTATION_PLAN.md`.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
{
"schema_version": 1,
"session_id": "20260715T120000Z_gate1_baseline",
"experiment": "autonomous_button_scan",
"tool": {
"version": "0.1.0",
"git_commit": "UNCOMMITTED"
},
"host": {
"macos_version": "26.5.1",
"machine": "arm64",
"python_version": "3.12.13",
"wifi_interface": "en0",
"vpn_active": true
},
"device": {
"model": "XGRIDS LixelKity K1",
"serial_suffix": null,
"activation_state": "unknown",
"firmware": null
},
"network": {
"topology": "unknown",
"k1_address_redacted": null
},
"timeline": [
{
"event": "operator_started_notes",
"monotonic_seconds": 0,
"utc": "2026-07-15T12:00:00Z",
"note": null
}
],
"artifacts": [],
"decision": {
"status": "PAUSE",
"gate": "gate1_device_baseline",
"reason": "Example only; no physical device observation has been performed.",
"next_smallest_experiment": "Record the K1 boot LED sequence without changing the router."
}
}

46
pyproject.toml Normal file
View File

@ -0,0 +1,46 @@
[build-system]
requires = ["hatchling>=1.27,<2"]
build-backend = "hatchling.build"
[project]
name = "ndc-xgrids-k1-connector"
version = "0.1.0"
description = "macOS-first, evidence-led research tooling for an XGRIDS Lixel K1"
readme = "README.md"
requires-python = ">=3.12,<3.13"
license = { text = "Proprietary" }
authors = [{ name = "NODE.DC" }]
dependencies = [
"rich>=13.9,<15",
"typer>=0.15,<1",
]
[project.scripts]
k1link = "k1link.cli:app"
[dependency-groups]
dev = [
"mypy>=1.15,<2",
"pytest>=8.3,<9",
"ruff>=0.11,<1",
]
[tool.hatch.build.targets.wheel]
packages = ["src/k1link"]
[tool.pytest.ini_options]
addopts = "-q"
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
[tool.mypy]
python_version = "3.12"
strict = true
packages = ["k1link"]

View File

@ -0,0 +1,113 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.dcserve.ru/SILVER/NDC_xgrids-k1-connector/schemas/session-manifest.schema.json",
"title": "K1 laboratory session manifest",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"session_id",
"experiment",
"tool",
"host",
"device",
"network",
"timeline",
"artifacts",
"decision"
],
"properties": {
"schema_version": { "const": 1 },
"session_id": {
"type": "string",
"pattern": "^[0-9]{8}T[0-9]{6}Z_[a-z0-9_-]+$"
},
"experiment": { "type": "string", "minLength": 1 },
"tool": {
"type": "object",
"additionalProperties": false,
"required": ["version", "git_commit"],
"properties": {
"version": { "type": "string" },
"git_commit": { "type": "string" }
}
},
"host": {
"type": "object",
"additionalProperties": false,
"required": ["macos_version", "machine", "python_version"],
"properties": {
"macos_version": { "type": "string" },
"machine": { "type": "string" },
"python_version": { "type": "string" },
"wifi_interface": { "type": ["string", "null"] },
"vpn_active": { "type": "boolean" }
}
},
"device": {
"type": "object",
"additionalProperties": false,
"required": ["model", "activation_state", "firmware"],
"properties": {
"model": { "type": "string" },
"serial_suffix": { "type": ["string", "null"], "maxLength": 8 },
"activation_state": {
"enum": ["unknown", "active", "activation_lock", "fault"]
},
"firmware": { "type": ["string", "null"] }
}
},
"network": {
"type": "object",
"additionalProperties": false,
"required": ["topology", "k1_address_redacted"],
"properties": {
"topology": {
"enum": ["unknown", "current_lan", "device_ap", "direct"]
},
"k1_address_redacted": { "type": ["string", "null"] }
}
},
"timeline": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["event", "monotonic_seconds"],
"properties": {
"event": { "type": "string", "minLength": 1 },
"monotonic_seconds": { "type": "number", "minimum": 0 },
"utc": { "type": ["string", "null"], "format": "date-time" },
"note": { "type": ["string", "null"] }
}
}
},
"artifacts": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["path", "size_bytes", "sha256", "classification"],
"properties": {
"path": { "type": "string", "minLength": 1 },
"size_bytes": { "type": "integer", "minimum": 0 },
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"classification": {
"enum": ["public", "redacted", "sensitive", "secret"]
}
}
}
},
"decision": {
"type": "object",
"additionalProperties": false,
"required": ["status", "gate", "reason"],
"properties": {
"status": { "enum": ["GO", "PAUSE", "BLOCKED"] },
"gate": { "type": "string", "minLength": 1 },
"reason": { "type": "string", "minLength": 1 },
"next_smallest_experiment": { "type": ["string", "null"] }
}
}
}
}

3
src/k1link/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""NDC XGRIDS K1 connector research tooling."""
__version__ = "0.1.0"

172
src/k1link/cli.py Normal file
View File

@ -0,0 +1,172 @@
from __future__ import annotations
import json
import platform
import shutil
import subprocess
import sys
from pathlib import Path
from typing import TypedDict
import typer
from rich.console import Console
from rich.table import Table
from k1link import __version__
app = typer.Typer(
name="k1link",
help="Safe, evidence-led research tooling for an owner-controlled XGRIDS K1.",
no_args_is_help=True,
)
console = Console()
class ToolStatus(TypedDict):
name: str
available: bool
path: str | None
class NetworkStatus(TypedDict):
wifi_interface: str | None
default_route_interface: str | None
vpn_default_route: bool
class DoctorPayload(TypedDict):
k1link_version: str
python_version: str
python_executable: str
python_is_3_12: bool
local_venv: bool
platform: str
machine: str
network: NetworkStatus
tools: list[ToolStatus]
notes: list[str]
def _tool_status(name: str) -> ToolStatus:
path = shutil.which(name)
return {"name": name, "available": path is not None, "path": path}
def _command_output(args: list[str]) -> str | None:
try:
result = subprocess.run(
args,
check=False,
capture_output=True,
text=True,
timeout=5,
)
except (OSError, subprocess.SubprocessError):
return None
if result.returncode != 0:
return None
return result.stdout.strip()
def _default_route_interface() -> str | None:
output = _command_output(["route", "-n", "get", "default"])
if output is None:
return None
for line in output.splitlines():
key, separator, value = line.strip().partition(":")
if separator and key == "interface":
return value.strip()
return None
def _wifi_interface() -> str | None:
output = _command_output(["networksetup", "-listallhardwareports"])
if output is None:
return None
blocks = output.split("\n\n")
for block in blocks:
if "Hardware Port: Wi-Fi" not in block:
continue
for line in block.splitlines():
key, separator, value = line.partition(":")
if separator and key.strip() == "Device":
return value.strip()
return None
def _doctor_payload() -> DoctorPayload:
executable = Path(sys.executable)
default_route = _default_route_interface()
wifi_interface = _wifi_interface()
notes = [
"Bluetooth permission is intentionally not requested by doctor.",
"Missing tshark/nmap is acceptable before the network-analysis gate.",
"No Homebrew or system changes are performed by this command.",
]
if default_route is not None and default_route.startswith("utun"):
notes.append(
"Default route uses a VPN/tunnel interface; future K1 commands must resolve "
"the route for the confirmed K1 IP instead of assuming the default route."
)
return {
"k1link_version": __version__,
"python_version": platform.python_version(),
"python_executable": str(executable),
"python_is_3_12": sys.version_info[:2] == (3, 12),
"local_venv": Path(sys.prefix).name == ".venv",
"platform": platform.platform(),
"machine": platform.machine(),
"network": {
"wifi_interface": wifi_interface,
"default_route_interface": default_route,
"vpn_default_route": bool(default_route and default_route.startswith("utun")),
},
"tools": [_tool_status(name) for name in ("uv", "tcpdump", "tshark", "nmap", "ffmpeg")],
"notes": notes,
}
@app.callback()
def main() -> None:
"""Run safe research commands for an owner-controlled XGRIDS K1."""
@app.command()
def doctor(
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
) -> None:
"""Inspect the local toolchain without touching the K1 or system configuration."""
payload = _doctor_payload()
if json_output:
typer.echo(json.dumps(payload, ensure_ascii=False, indent=2))
return
console.print(f"k1link {payload['k1link_version']}")
console.print(f"Python {payload['python_version']} ({payload['python_executable']})")
console.print(
"Local .venv: " + ("[green]yes[/green]" if payload["local_venv"] else "[red]no[/red]")
)
network = payload["network"]
console.print(
"Wi-Fi interface: "
f"{network['wifi_interface'] or '-'}; default route: "
f"{network['default_route_interface'] or '-'}"
)
table = Table(title="External tools")
table.add_column("Tool")
table.add_column("Available")
table.add_column("Path")
for item in payload["tools"]:
table.add_row(
str(item["name"]),
"yes" if item["available"] else "no",
str(item["path"] or "-"),
)
console.print(table)
for note in payload["notes"]:
console.print(f"- {note}")
if __name__ == "__main__":
app()

23
tests/test_cli.py Normal file
View File

@ -0,0 +1,23 @@
import json
from typer.testing import CliRunner
from k1link.cli import app
runner = CliRunner()
def test_help() -> None:
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "evidence-led" in result.stdout
def test_doctor_json() -> None:
result = runner.invoke(app, ["doctor", "--json"])
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["k1link_version"] == "0.1.0"
assert isinstance(payload["tools"], list)
assert isinstance(payload["network"], dict)
assert any(item["name"] == "tcpdump" for item in payload["tools"])

View File

@ -0,0 +1,40 @@
import hashlib
import json
from pathlib import Path
ROOT = Path(__file__).parents[1]
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def test_reference_inputs_are_unchanged() -> None:
expected = {
"CODEX_K1_MAC_LINK_PREPROD_PROMPT.md": (
"f24537119bd76f76d1914ca7f34a6736d8c5d3b6d19ead6d0ae8a0e5966c9770"
),
"XGRIDS_Lixel_K1_PreProduction_Bible.md": (
"22ea9d0dc6f5a623ba26d6f08108a905170db08112e0531a1d80bddd6101689c"
),
}
for filename, expected_hash in expected.items():
assert _sha256(ROOT / "docs" / "reference" / filename) == expected_hash
def test_redacted_manifest_example_is_json() -> None:
path = ROOT / "examples" / "session_manifest.redacted.json"
payload = json.loads(path.read_text(encoding="utf-8"))
assert payload["schema_version"] == 1
assert payload["decision"]["status"] in {"GO", "PAUSE", "BLOCKED"}
def test_manifest_schema_is_json() -> None:
path = ROOT / "schemas" / "session-manifest.schema.json"
payload = json.loads(path.read_text(encoding="utf-8"))
assert payload["$schema"] == "https://json-schema.org/draft/2020-12/schema"
assert payload["properties"]["schema_version"]["const"] == 1

255
uv.lock Normal file
View File

@ -0,0 +1,255 @@
version = 1
revision = 3
requires-python = "==3.12.*"
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "librt"
version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" },
{ url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" },
{ url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" },
{ url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" },
{ url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" },
{ url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" },
{ url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" },
{ url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" },
{ url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" },
{ url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" },
{ url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" },
{ url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mypy"
version = "1.20.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" },
{ url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" },
{ url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" },
{ url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" },
{ url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" },
{ url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" },
{ url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" },
{ url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "ndc-xgrids-k1-connector"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "rich" },
{ name = "typer" },
]
[package.dev-dependencies]
dev = [
{ name = "mypy" },
{ name = "pytest" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "rich", specifier = ">=13.9,<15" },
{ name = "typer", specifier = ">=0.15,<1" },
]
[package.metadata.requires-dev]
dev = [
{ name = "mypy", specifier = ">=1.15,<2" },
{ name = "pytest", specifier = ">=8.3,<9" },
{ name = "ruff", specifier = ">=0.11,<1" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pathspec"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pytest"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "rich"
version = "14.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" },
]
[[package]]
name = "ruff"
version = "0.15.21"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" },
{ url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" },
{ url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" },
{ url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" },
{ url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" },
{ url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" },
{ url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" },
{ url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" },
{ url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" },
{ url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" },
{ url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" },
{ url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" },
{ url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" },
{ url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "typer"
version = "0.26.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]