buildai-cli 0.3.87__tar.gz → 0.3.89__tar.gz
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.
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/.gitignore +5 -0
- buildai_cli-0.3.89/AGENTS.md +44 -0
- buildai_cli-0.3.89/CLAUDE.md +1 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/PKG-INFO +1 -1
- buildai_cli-0.3.89/cli/commands/ego_frame_search.py +569 -0
- buildai_cli-0.3.89/cli/commands/spec.py +173 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/main.py +5 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/pyproject.toml +1 -1
- buildai_cli-0.3.87/AGENTS.md +0 -1
- buildai_cli-0.3.87/CLAUDE.md +0 -47
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/__init__.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/auth_local.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/db/broker.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/db/common.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/db/migrate.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/db/tunnel.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/egoexo.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/gigcamera.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/grid.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/ingest.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/ingest_docs.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/commands/processing.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/config.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/console.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/context.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/db_broker.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/guard.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/output.py +0 -0
- {buildai_cli-0.3.87 → buildai_cli-0.3.89}/cli/pagination.py +0 -0
|
@@ -21,6 +21,8 @@ dist/
|
|
|
21
21
|
!apps/buildai-openai-ingest/frontend/dist/
|
|
22
22
|
!apps/buildai-openai-ingest/frontend/dist/index.html
|
|
23
23
|
!apps/buildai-openai-ingest/frontend/dist/mac-mini.png
|
|
24
|
+
# Vendored BuildSpec engine — commit its built dist; node_modules stays ignored.
|
|
25
|
+
!tools/buildspec/dist/
|
|
24
26
|
build/
|
|
25
27
|
.pytest_cache/
|
|
26
28
|
.mypy_cache/
|
|
@@ -106,6 +108,9 @@ recording-cache/
|
|
|
106
108
|
/datasets/250k-hour-dataset/build/**/out/
|
|
107
109
|
/datasets/imu-on-head-frames-2026-04-25/*
|
|
108
110
|
!/datasets/imu-on-head-frames-2026-04-25/README.md
|
|
111
|
+
!/datasets/golden-ego-exo/build/
|
|
112
|
+
!/datasets/golden-ego-exo/build/**
|
|
113
|
+
/datasets/golden-ego-exo/build/**/out/
|
|
109
114
|
/datasets/dataset-*.json
|
|
110
115
|
/datasets/**/__pycache__/
|
|
111
116
|
/datasets/**/frames/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# BuildAI CLI — Typer CLI with standalone (API-backed) and workspace (DB-direct) modes.
|
|
2
|
+
|
|
3
|
+
## Read first
|
|
4
|
+
|
|
5
|
+
- [../../AGENTS.md](../../AGENTS.md)
|
|
6
|
+
- Docs site Engineering section (`make docs`)
|
|
7
|
+
- Docs site Operations > Database roles (`make docs`)
|
|
8
|
+
|
|
9
|
+
## Rules
|
|
10
|
+
|
|
11
|
+
**Two planes.**
|
|
12
|
+
|
|
13
|
+
| Mode | Install | Auth | DB access |
|
|
14
|
+
|------|---------|------|-----------|
|
|
15
|
+
| Standalone (`buildai`) | `uv tool install buildai-cli` | API key / JWT | Through API |
|
|
16
|
+
| Workspace (`uv run buildai`) | Repo-local editable install | IAM / password | Direct via `db`, through profile-keyed local broker |
|
|
17
|
+
|
|
18
|
+
**DB guards.** `db` subcommands require workspace install + gcloud IAM. Inspection/migration uses a profile-keyed `alloydb-auth-proxy` broker with state in `~/.buildai/db-brokers` (shared across worktrees). Use `buildai db broker status|ensure|stop` for local transport.
|
|
19
|
+
|
|
20
|
+
**Writes.** Require `--write`. Production migrations prompt for confirmation.
|
|
21
|
+
|
|
22
|
+
**Targeting.** Worktree app/runtime targeting comes from explicit env vars and the repo Makefile, not a saved CLI context layer. `buildai db --env` selects `production`, `staging`, or `dev`.
|
|
23
|
+
|
|
24
|
+
**Staging.** Private-IP only. Start `buildai db --env staging tunnel`, then pass `--all-proxy socks5://127.0.0.1:1080` for queries.
|
|
25
|
+
|
|
26
|
+
**Workers.** Call the API through the SDK; do not import `core/` or access the database directly from worker shells.
|
|
27
|
+
|
|
28
|
+
## Verify
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
buildai auth whoami
|
|
32
|
+
uv run buildai ingest deploy plan --scope active
|
|
33
|
+
buildai db broker status
|
|
34
|
+
buildai db schema tables
|
|
35
|
+
buildai db query "SELECT count(*) FROM core.clips"
|
|
36
|
+
buildai db --env staging tunnel # in one terminal; query with --all-proxy in another
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Don't
|
|
40
|
+
|
|
41
|
+
- Run `db --write` without `--write`.
|
|
42
|
+
- Query staging without an IAP/VPN/SOCKS route and `--all-proxy`.
|
|
43
|
+
- Assume a saved CLI context picks the worktree's DB lane — use env vars and Makefile.
|
|
44
|
+
- Deploy ingest fleet changes from an unclear branch or dirty checkout.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"""Operator commands for the reusable ego frame-search embedding space."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import asdict, is_dataclass
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from cli.context import get_cli_context
|
|
12
|
+
from cli.ops_init import init_ops_context
|
|
13
|
+
from cli.output import Format, format_option, render
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(no_args_is_help=True, help="Operate ego frame-search embeddings.")
|
|
16
|
+
_SUBMITTED_BY = "00000000-0000-0000-0000-000000000000"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback()
|
|
20
|
+
def ego_frame_search_callback(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
env: str | None = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--env",
|
|
25
|
+
"-e",
|
|
26
|
+
help="Target environment. Defaults to APP_ENV, then production.",
|
|
27
|
+
),
|
|
28
|
+
auth: str = typer.Option(None, "--auth", "-a", help="Auth method: iam or password."),
|
|
29
|
+
user: str = typer.Option(None, "--user", "-u", help="Override database user."),
|
|
30
|
+
profile: str | None = typer.Option(
|
|
31
|
+
None,
|
|
32
|
+
"--profile",
|
|
33
|
+
"-p",
|
|
34
|
+
help="Auth workflow profile. Use internal_admin for writes.",
|
|
35
|
+
),
|
|
36
|
+
all_proxy: str | None = typer.Option(
|
|
37
|
+
None,
|
|
38
|
+
"--all-proxy",
|
|
39
|
+
help="Proxy URL passed to alloydb-auth-proxy for private-only DB lanes.",
|
|
40
|
+
),
|
|
41
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging."),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Capture DB-targeting flags; subcommands initialize ops context lazily."""
|
|
44
|
+
ctx.ensure_object(dict)
|
|
45
|
+
ctx.obj["cli_profile"] = profile
|
|
46
|
+
ctx.obj["_env"] = env
|
|
47
|
+
ctx.obj["_auth"] = auth
|
|
48
|
+
ctx.obj["_user"] = user
|
|
49
|
+
ctx.obj["_all_proxy"] = all_proxy
|
|
50
|
+
ctx.obj["_verbose"] = verbose
|
|
51
|
+
ctx.obj.setdefault("allow_write", False)
|
|
52
|
+
ctx.obj.setdefault("_ops_ready", False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _settings_for_command(ctx: typer.Context, *, write: bool) -> object:
|
|
56
|
+
"""Initialize the repo DB settings for one command."""
|
|
57
|
+
ctx.ensure_object(dict)
|
|
58
|
+
ctx.obj["allow_write"] = write
|
|
59
|
+
if "settings" in ctx.obj:
|
|
60
|
+
return ctx.obj["settings"]
|
|
61
|
+
return init_ops_context(ctx)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _require_internal_admin_for_write(ctx: typer.Context, *, write: bool) -> None:
|
|
65
|
+
"""Require the explicit admin profile before direct DB writes."""
|
|
66
|
+
if not write:
|
|
67
|
+
return
|
|
68
|
+
profile = (ctx.obj or {}).get("cli_profile") or "engineers-dev"
|
|
69
|
+
if profile != "internal_admin":
|
|
70
|
+
raise typer.BadParameter("writes require --profile internal_admin")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _plain(value: object) -> object:
|
|
74
|
+
"""Convert dataclass outputs into JSON/table-friendly dictionaries."""
|
|
75
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
76
|
+
return asdict(value)
|
|
77
|
+
if isinstance(value, list):
|
|
78
|
+
return [_plain(item) for item in value]
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def _resolve_space_id(dal_ctx: object, *, space_id: int | None, space_key: str | None) -> int:
|
|
83
|
+
"""Resolve one space id from either numeric id or semantic key."""
|
|
84
|
+
from dal.embeddings import spaces as spaces_dal
|
|
85
|
+
|
|
86
|
+
if space_id is not None and space_key is not None:
|
|
87
|
+
raise typer.BadParameter("pass either --space-id or --space-key, not both")
|
|
88
|
+
if space_id is not None:
|
|
89
|
+
return space_id
|
|
90
|
+
if space_key is None:
|
|
91
|
+
space_key = "qwen.frame.global.ego.default"
|
|
92
|
+
space = await spaces_dal.get_by_space_key(dal_ctx, space_key)
|
|
93
|
+
return space.id
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("corpora")
|
|
97
|
+
def corpora(
|
|
98
|
+
ctx: typer.Context,
|
|
99
|
+
format: Format = format_option(),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""List registered ego frame-search corpora and route flags."""
|
|
102
|
+
settings = _settings_for_command(ctx, write=False)
|
|
103
|
+
|
|
104
|
+
async def run() -> None:
|
|
105
|
+
from dal.embeddings import ego_frame_search as ego_search_dal
|
|
106
|
+
|
|
107
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
108
|
+
_db,
|
|
109
|
+
dal_ctx,
|
|
110
|
+
):
|
|
111
|
+
rows = await ego_search_dal.list_corpora(dal_ctx)
|
|
112
|
+
render(rows, format=format)
|
|
113
|
+
|
|
114
|
+
asyncio.run(run())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command("membership-program")
|
|
118
|
+
def membership_program(
|
|
119
|
+
ctx: typer.Context,
|
|
120
|
+
corpus_key: str = typer.Option(..., "--corpus-key", help="Target corpus key."),
|
|
121
|
+
program_id: list[UUID] = typer.Option(..., "--program-id", help="Program id to include."),
|
|
122
|
+
run_id: str = typer.Option("manual", "--run-id", help="Membership materialization run id."),
|
|
123
|
+
limit: int | None = typer.Option(None, "--limit", min=1, help="Cap candidate clips."),
|
|
124
|
+
write: bool = typer.Option(False, "--write", help="Insert membership rows."),
|
|
125
|
+
format: Format = format_option(),
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Dry-run or materialize corpus membership from program-source clips."""
|
|
128
|
+
_require_internal_admin_for_write(ctx, write=write)
|
|
129
|
+
settings = _settings_for_command(ctx, write=write)
|
|
130
|
+
|
|
131
|
+
async def run() -> None:
|
|
132
|
+
from dal.embeddings import ego_frame_search as ego_search_dal
|
|
133
|
+
|
|
134
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
135
|
+
_db,
|
|
136
|
+
dal_ctx,
|
|
137
|
+
):
|
|
138
|
+
summary = await ego_search_dal.materialize_program_membership(
|
|
139
|
+
dal_ctx,
|
|
140
|
+
corpus_key=corpus_key,
|
|
141
|
+
program_ids=program_id,
|
|
142
|
+
membership_run_id=run_id,
|
|
143
|
+
limit=limit,
|
|
144
|
+
write=write,
|
|
145
|
+
)
|
|
146
|
+
render(_plain(summary), format=format)
|
|
147
|
+
|
|
148
|
+
asyncio.run(run())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@app.command("membership-gigcamera")
|
|
152
|
+
def membership_gigcamera(
|
|
153
|
+
ctx: typer.Context,
|
|
154
|
+
corpus_key: str = typer.Option("gigcamera", "--corpus-key", help="Target corpus key."),
|
|
155
|
+
run_id: str = typer.Option("manual", "--run-id", help="Membership materialization run id."),
|
|
156
|
+
limit: int | None = typer.Option(None, "--limit", min=1, help="Cap candidate clips."),
|
|
157
|
+
write: bool = typer.Option(False, "--write", help="Insert membership rows."),
|
|
158
|
+
format: Format = format_option(),
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Dry-run or materialize non-duplicate GigCamera corpus membership."""
|
|
161
|
+
_require_internal_admin_for_write(ctx, write=write)
|
|
162
|
+
settings = _settings_for_command(ctx, write=write)
|
|
163
|
+
|
|
164
|
+
async def run() -> None:
|
|
165
|
+
from dal.embeddings import ego_frame_search as ego_search_dal
|
|
166
|
+
|
|
167
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
168
|
+
_db,
|
|
169
|
+
dal_ctx,
|
|
170
|
+
):
|
|
171
|
+
summary = await ego_search_dal.materialize_gigcamera_membership(
|
|
172
|
+
dal_ctx,
|
|
173
|
+
corpus_key=corpus_key,
|
|
174
|
+
membership_run_id=run_id,
|
|
175
|
+
limit=limit,
|
|
176
|
+
write=write,
|
|
177
|
+
)
|
|
178
|
+
render(_plain(summary), format=format)
|
|
179
|
+
|
|
180
|
+
asyncio.run(run())
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command("coverage")
|
|
184
|
+
def coverage(
|
|
185
|
+
ctx: typer.Context,
|
|
186
|
+
corpus_key: str = typer.Option(..., "--corpus-key", help="Corpus key to inspect."),
|
|
187
|
+
space_id: int | None = typer.Option(None, "--space-id", help="Target embedding space id."),
|
|
188
|
+
space_key: str | None = typer.Option(
|
|
189
|
+
"qwen.frame.global.ego.default",
|
|
190
|
+
"--space-key",
|
|
191
|
+
help="Target embedding space key.",
|
|
192
|
+
),
|
|
193
|
+
selection_kind: str = typer.Option("near_90s", "--selection-kind"),
|
|
194
|
+
refresh: bool = typer.Option(False, "--refresh", help="Upsert readiness counts."),
|
|
195
|
+
route_ready: bool = typer.Option(False, "--route-ready", help="Mark routing verified."),
|
|
196
|
+
smoke_passed: bool = typer.Option(False, "--smoke-passed", help="Mark smoke benchmark passed."),
|
|
197
|
+
write: bool = typer.Option(False, "--write", help="Apply readiness refresh."),
|
|
198
|
+
format: Format = format_option(),
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Inspect or refresh corpus coverage/readiness for a target space."""
|
|
201
|
+
write_requested = write or refresh
|
|
202
|
+
_require_internal_admin_for_write(ctx, write=write_requested)
|
|
203
|
+
settings = _settings_for_command(ctx, write=write_requested)
|
|
204
|
+
|
|
205
|
+
async def run() -> None:
|
|
206
|
+
from dal.embeddings import ego_frame_search as ego_search_dal
|
|
207
|
+
|
|
208
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
209
|
+
_db,
|
|
210
|
+
dal_ctx,
|
|
211
|
+
):
|
|
212
|
+
target_space_id = await _resolve_space_id(
|
|
213
|
+
dal_ctx,
|
|
214
|
+
space_id=space_id,
|
|
215
|
+
space_key=space_key,
|
|
216
|
+
)
|
|
217
|
+
if refresh:
|
|
218
|
+
summary = await ego_search_dal.refresh_corpus_readiness(
|
|
219
|
+
dal_ctx,
|
|
220
|
+
corpus_key=corpus_key,
|
|
221
|
+
target_space_id=target_space_id,
|
|
222
|
+
selection_kind=selection_kind,
|
|
223
|
+
route_ready=route_ready,
|
|
224
|
+
smoke_passed=smoke_passed,
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
summary = await ego_search_dal.compute_corpus_coverage(
|
|
228
|
+
dal_ctx,
|
|
229
|
+
corpus_key=corpus_key,
|
|
230
|
+
target_space_id=target_space_id,
|
|
231
|
+
selection_kind=selection_kind,
|
|
232
|
+
)
|
|
233
|
+
render(_plain(summary), format=format)
|
|
234
|
+
|
|
235
|
+
asyncio.run(run())
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command("missing-vectors")
|
|
239
|
+
def missing_vectors(
|
|
240
|
+
ctx: typer.Context,
|
|
241
|
+
corpus_key: str = typer.Option(..., "--corpus-key", help="Corpus key to inspect."),
|
|
242
|
+
space_id: int | None = typer.Option(None, "--space-id", help="Target embedding space id."),
|
|
243
|
+
space_key: str | None = typer.Option("qwen.frame.global.ego.default", "--space-key"),
|
|
244
|
+
selection_kind: str = typer.Option("near_90s", "--selection-kind"),
|
|
245
|
+
format: Format = format_option(),
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Count sampled corpus frames that still need embeddings."""
|
|
248
|
+
settings = _settings_for_command(ctx, write=False)
|
|
249
|
+
|
|
250
|
+
async def run() -> None:
|
|
251
|
+
from dal.embeddings import ego_frame_search as ego_search_dal
|
|
252
|
+
|
|
253
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
254
|
+
_db,
|
|
255
|
+
dal_ctx,
|
|
256
|
+
):
|
|
257
|
+
target_space_id = await _resolve_space_id(
|
|
258
|
+
dal_ctx,
|
|
259
|
+
space_id=space_id,
|
|
260
|
+
space_key=space_key,
|
|
261
|
+
)
|
|
262
|
+
count = await ego_search_dal.count_missing_vectors(
|
|
263
|
+
dal_ctx,
|
|
264
|
+
corpus_key=corpus_key,
|
|
265
|
+
target_space_id=target_space_id,
|
|
266
|
+
selection_kind=selection_kind,
|
|
267
|
+
)
|
|
268
|
+
render(
|
|
269
|
+
{
|
|
270
|
+
"corpus_key": corpus_key,
|
|
271
|
+
"target_space_id": target_space_id,
|
|
272
|
+
"selection_kind": selection_kind,
|
|
273
|
+
"missing_vector_count": count,
|
|
274
|
+
},
|
|
275
|
+
format=format,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
asyncio.run(run())
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@app.command("missing-demian-labels")
|
|
282
|
+
def missing_demian_labels(
|
|
283
|
+
ctx: typer.Context,
|
|
284
|
+
corpus_key: str = typer.Option(..., "--corpus-key", help="Corpus key to inspect."),
|
|
285
|
+
selection_kind: str = typer.Option("near_90s", "--selection-kind"),
|
|
286
|
+
observation_kind: str = typer.Option("demian_frame_label_v1", "--observation-kind"),
|
|
287
|
+
format: Format = format_option(),
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Count sampled corpus frames missing DeMiAn labels."""
|
|
290
|
+
settings = _settings_for_command(ctx, write=False)
|
|
291
|
+
|
|
292
|
+
async def run() -> None:
|
|
293
|
+
from dal.embeddings import ego_frame_search as ego_search_dal
|
|
294
|
+
|
|
295
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
296
|
+
_db,
|
|
297
|
+
dal_ctx,
|
|
298
|
+
):
|
|
299
|
+
count = await ego_search_dal.count_missing_demian_labels(
|
|
300
|
+
dal_ctx,
|
|
301
|
+
corpus_key=corpus_key,
|
|
302
|
+
selection_kind=selection_kind,
|
|
303
|
+
observation_kind=observation_kind,
|
|
304
|
+
)
|
|
305
|
+
render(
|
|
306
|
+
{
|
|
307
|
+
"corpus_key": corpus_key,
|
|
308
|
+
"selection_kind": selection_kind,
|
|
309
|
+
"observation_kind": observation_kind,
|
|
310
|
+
"missing_demian_label_count": count,
|
|
311
|
+
},
|
|
312
|
+
format=format,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
asyncio.run(run())
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@app.command("copy-plan")
|
|
319
|
+
def copy_plan(
|
|
320
|
+
ctx: typer.Context,
|
|
321
|
+
source_space_id: int = typer.Option(..., "--source-space-id"),
|
|
322
|
+
target_space_id: int | None = typer.Option(None, "--target-space-id"),
|
|
323
|
+
target_space_key: str | None = typer.Option(
|
|
324
|
+
"qwen.frame.global.ego.default", "--target-space-key"
|
|
325
|
+
),
|
|
326
|
+
selection_kind: str = typer.Option("near_90s", "--selection-kind"),
|
|
327
|
+
format: Format = format_option(),
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Inspect how many legacy vectors can be copied safely."""
|
|
330
|
+
settings = _settings_for_command(ctx, write=False)
|
|
331
|
+
|
|
332
|
+
async def run() -> None:
|
|
333
|
+
from dal.embeddings import ego_frame_search as ego_search_dal
|
|
334
|
+
|
|
335
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
336
|
+
_db,
|
|
337
|
+
dal_ctx,
|
|
338
|
+
):
|
|
339
|
+
resolved_target_space_id = await _resolve_space_id(
|
|
340
|
+
dal_ctx,
|
|
341
|
+
space_id=target_space_id,
|
|
342
|
+
space_key=target_space_key,
|
|
343
|
+
)
|
|
344
|
+
plan = await ego_search_dal.plan_vector_copy(
|
|
345
|
+
dal_ctx,
|
|
346
|
+
source_space_id=source_space_id,
|
|
347
|
+
target_space_id=resolved_target_space_id,
|
|
348
|
+
selection_kind=selection_kind,
|
|
349
|
+
)
|
|
350
|
+
render(_plain(plan), format=format)
|
|
351
|
+
|
|
352
|
+
asyncio.run(run())
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@app.command("copy-vectors")
|
|
356
|
+
def copy_vectors(
|
|
357
|
+
ctx: typer.Context,
|
|
358
|
+
source_space_id: int = typer.Option(..., "--source-space-id"),
|
|
359
|
+
target_space_id: int | None = typer.Option(None, "--target-space-id"),
|
|
360
|
+
target_space_key: str | None = typer.Option(
|
|
361
|
+
"qwen.frame.global.ego.default", "--target-space-key"
|
|
362
|
+
),
|
|
363
|
+
selection_kind: str = typer.Option("near_90s", "--selection-kind"),
|
|
364
|
+
run_id: str = typer.Option(..., "--run-id", help="Operator copy run id."),
|
|
365
|
+
limit: int | None = typer.Option(None, "--limit", min=1, help="Cap copied vectors."),
|
|
366
|
+
write: bool = typer.Option(False, "--write", help="Insert copied vector rows."),
|
|
367
|
+
format: Format = format_option(),
|
|
368
|
+
) -> None:
|
|
369
|
+
"""Copy compatible legacy vectors without overwriting target rows."""
|
|
370
|
+
_require_internal_admin_for_write(ctx, write=write)
|
|
371
|
+
settings = _settings_for_command(ctx, write=write)
|
|
372
|
+
|
|
373
|
+
async def run() -> None:
|
|
374
|
+
from dal.embeddings import ego_frame_search as ego_search_dal
|
|
375
|
+
from dal.embeddings import spaces as spaces_dal
|
|
376
|
+
|
|
377
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
378
|
+
_db,
|
|
379
|
+
dal_ctx,
|
|
380
|
+
):
|
|
381
|
+
resolved_target_space_id = await _resolve_space_id(
|
|
382
|
+
dal_ctx,
|
|
383
|
+
space_id=target_space_id,
|
|
384
|
+
space_key=target_space_key,
|
|
385
|
+
)
|
|
386
|
+
source_space = await spaces_dal.get(dal_ctx, source_space_id)
|
|
387
|
+
target_space = await spaces_dal.get(dal_ctx, resolved_target_space_id)
|
|
388
|
+
compatibility = ego_search_dal.compare_spaces(
|
|
389
|
+
source=source_space,
|
|
390
|
+
target=target_space,
|
|
391
|
+
)
|
|
392
|
+
if not compatibility.compatible:
|
|
393
|
+
raise typer.BadParameter("; ".join(compatibility.reasons))
|
|
394
|
+
plan = await ego_search_dal.plan_vector_copy(
|
|
395
|
+
dal_ctx,
|
|
396
|
+
source_space_id=source_space_id,
|
|
397
|
+
target_space_id=resolved_target_space_id,
|
|
398
|
+
selection_kind=selection_kind,
|
|
399
|
+
)
|
|
400
|
+
inserted = 0
|
|
401
|
+
if write:
|
|
402
|
+
inserted = await ego_search_dal.copy_compatible_vectors(
|
|
403
|
+
dal_ctx,
|
|
404
|
+
source_space_id=source_space_id,
|
|
405
|
+
target_space_id=resolved_target_space_id,
|
|
406
|
+
selection_kind=selection_kind,
|
|
407
|
+
copy_run_id=run_id,
|
|
408
|
+
limit=limit,
|
|
409
|
+
)
|
|
410
|
+
render(
|
|
411
|
+
{
|
|
412
|
+
"write": write,
|
|
413
|
+
"inserted": inserted,
|
|
414
|
+
"compatibility": _plain(compatibility),
|
|
415
|
+
"plan": _plain(plan),
|
|
416
|
+
},
|
|
417
|
+
format=format,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
asyncio.run(run())
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@app.command("build-scann")
|
|
424
|
+
def build_scann(
|
|
425
|
+
ctx: typer.Context,
|
|
426
|
+
space_id: int | None = typer.Option(None, "--space-id", help="Target embedding space id."),
|
|
427
|
+
space_key: str | None = typer.Option("qwen.frame.global.ego.default", "--space-key"),
|
|
428
|
+
strategy: str = typer.Option("balanced", "--strategy", help="speed, balanced, or quality."),
|
|
429
|
+
quantizer: str = typer.Option("SQ8", "--quantizer", help="SQ8, FLAT, or AH."),
|
|
430
|
+
write: bool = typer.Option(False, "--write", help="Create btree/ANALYZE/ScaNN."),
|
|
431
|
+
format: Format = format_option(),
|
|
432
|
+
) -> None:
|
|
433
|
+
"""Analyze a partition and create its ScaNN index after bulk load."""
|
|
434
|
+
_require_internal_admin_for_write(ctx, write=write)
|
|
435
|
+
settings = _settings_for_command(ctx, write=write)
|
|
436
|
+
|
|
437
|
+
async def run() -> None:
|
|
438
|
+
from dal.embeddings import indexes as index_dal
|
|
439
|
+
|
|
440
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
441
|
+
_db,
|
|
442
|
+
dal_ctx,
|
|
443
|
+
):
|
|
444
|
+
target_space_id = await _resolve_space_id(
|
|
445
|
+
dal_ctx,
|
|
446
|
+
space_id=space_id,
|
|
447
|
+
space_key=space_key,
|
|
448
|
+
)
|
|
449
|
+
info = await index_dal.create_scann_index(
|
|
450
|
+
dal_ctx,
|
|
451
|
+
target_space_id,
|
|
452
|
+
strategy=strategy, # type: ignore[arg-type]
|
|
453
|
+
quantizer=quantizer, # type: ignore[arg-type]
|
|
454
|
+
dry_run=not write,
|
|
455
|
+
)
|
|
456
|
+
render(info, format=format)
|
|
457
|
+
|
|
458
|
+
asyncio.run(run())
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@app.command("queue-missing-embeddings")
|
|
462
|
+
def queue_missing_embeddings(
|
|
463
|
+
ctx: typer.Context,
|
|
464
|
+
corpus_key: list[str] = typer.Option(..., "--corpus-key", help="Corpus key. Repeatable."),
|
|
465
|
+
space_id: int | None = typer.Option(None, "--space-id", help="Target embedding space id."),
|
|
466
|
+
space_key: str | None = typer.Option("qwen.frame.global.ego.default", "--space-key"),
|
|
467
|
+
selection_kind: str = typer.Option("near_90s", "--selection-kind"),
|
|
468
|
+
limit: int | None = typer.Option(None, "--limit", min=1, help="Cap selected sampled frames."),
|
|
469
|
+
idempotency_key: str = typer.Option(..., "--idempotency-key", help="Manifest idempotency key."),
|
|
470
|
+
cost_cap_usd: float | None = typer.Option(None, "--cost-cap-usd", min=0.0),
|
|
471
|
+
write: bool = typer.Option(False, "--write", help="Create the processing manifest."),
|
|
472
|
+
format: Format = format_option(),
|
|
473
|
+
) -> None:
|
|
474
|
+
"""Queue a capped missing-only embed_frames manifest from corpus membership."""
|
|
475
|
+
_require_internal_admin_for_write(ctx, write=write)
|
|
476
|
+
settings = _settings_for_command(ctx, write=write)
|
|
477
|
+
|
|
478
|
+
async def run() -> None:
|
|
479
|
+
from dal.processing import media_jobs
|
|
480
|
+
|
|
481
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
482
|
+
_db,
|
|
483
|
+
dal_ctx,
|
|
484
|
+
):
|
|
485
|
+
target_space_id = await _resolve_space_id(
|
|
486
|
+
dal_ctx,
|
|
487
|
+
space_id=space_id,
|
|
488
|
+
space_key=space_key,
|
|
489
|
+
)
|
|
490
|
+
selection_spec = {
|
|
491
|
+
"kind": "by_corpus",
|
|
492
|
+
"corpus_keys": corpus_key,
|
|
493
|
+
"selection_kind": selection_kind,
|
|
494
|
+
"limit": limit,
|
|
495
|
+
"only_missing_vectors": True,
|
|
496
|
+
}
|
|
497
|
+
sink_overrides = {"embedding": {"space_id": target_space_id}}
|
|
498
|
+
resource_overrides = {"cost_cap_usd": cost_cap_usd} if cost_cap_usd is not None else {}
|
|
499
|
+
if write:
|
|
500
|
+
manifest = await media_jobs.queue_processor_job(
|
|
501
|
+
dal_ctx,
|
|
502
|
+
processor_ref=media_jobs.EMBED_FRAMES_PROCESSOR_REF,
|
|
503
|
+
selection_spec=selection_spec,
|
|
504
|
+
sink_overrides=sink_overrides,
|
|
505
|
+
resource_overrides=resource_overrides,
|
|
506
|
+
submitted_by_principal=_SUBMITTED_BY,
|
|
507
|
+
idempotency_key=idempotency_key,
|
|
508
|
+
)
|
|
509
|
+
else:
|
|
510
|
+
manifest = {
|
|
511
|
+
"dry_run": True,
|
|
512
|
+
"processor_ref": media_jobs.EMBED_FRAMES_PROCESSOR_REF,
|
|
513
|
+
"selection_spec": selection_spec,
|
|
514
|
+
"sink_overrides": sink_overrides,
|
|
515
|
+
"resource_overrides": resource_overrides,
|
|
516
|
+
}
|
|
517
|
+
render(manifest, format=format)
|
|
518
|
+
|
|
519
|
+
asyncio.run(run())
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@app.command("queue-missing-sampled-frames")
|
|
523
|
+
def queue_missing_sampled_frames(
|
|
524
|
+
ctx: typer.Context,
|
|
525
|
+
corpus_key: list[str] = typer.Option(..., "--corpus-key", help="Corpus key. Repeatable."),
|
|
526
|
+
selection_kind: str = typer.Option("near_90s", "--selection-kind"),
|
|
527
|
+
limit: int | None = typer.Option(None, "--limit", min=1, help="Cap selected clips."),
|
|
528
|
+
idempotency_key: str = typer.Option(..., "--idempotency-key", help="Manifest idempotency key."),
|
|
529
|
+
write: bool = typer.Option(False, "--write", help="Create the processing manifest."),
|
|
530
|
+
format: Format = format_option(),
|
|
531
|
+
) -> None:
|
|
532
|
+
"""Queue extraction for corpus clips missing the sampled-frame anchor."""
|
|
533
|
+
_require_internal_admin_for_write(ctx, write=write)
|
|
534
|
+
settings = _settings_for_command(ctx, write=write)
|
|
535
|
+
|
|
536
|
+
async def run() -> None:
|
|
537
|
+
from dal.processing import media_jobs
|
|
538
|
+
|
|
539
|
+
async with get_cli_context(settings, profile=(ctx.obj or {}).get("cli_profile")) as (
|
|
540
|
+
_db,
|
|
541
|
+
dal_ctx,
|
|
542
|
+
):
|
|
543
|
+
selection_spec = {
|
|
544
|
+
"kind": "by_corpus",
|
|
545
|
+
"corpus_keys": corpus_key,
|
|
546
|
+
"selection_kind": selection_kind,
|
|
547
|
+
"limit": limit,
|
|
548
|
+
"only_missing_sampled_frames": True,
|
|
549
|
+
"require_zero_frames": False,
|
|
550
|
+
}
|
|
551
|
+
if write:
|
|
552
|
+
manifest = await media_jobs.queue_processor_job(
|
|
553
|
+
dal_ctx,
|
|
554
|
+
processor_ref=media_jobs.EXTRACT_FRAMES_PROCESSOR_REF,
|
|
555
|
+
selection_spec=selection_spec,
|
|
556
|
+
sink_overrides=None,
|
|
557
|
+
resource_overrides=None,
|
|
558
|
+
submitted_by_principal=_SUBMITTED_BY,
|
|
559
|
+
idempotency_key=idempotency_key,
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
manifest = {
|
|
563
|
+
"dry_run": True,
|
|
564
|
+
"processor_ref": media_jobs.EXTRACT_FRAMES_PROCESSOR_REF,
|
|
565
|
+
"selection_spec": selection_spec,
|
|
566
|
+
}
|
|
567
|
+
render(manifest, format=format)
|
|
568
|
+
|
|
569
|
+
asyncio.run(run())
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""BuildSpec wrapper commands for the Build AI CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from cli.console import error, info
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _repo_root() -> Path | None:
|
|
16
|
+
"""Return the current Build AI repo root when running from a checkout."""
|
|
17
|
+
|
|
18
|
+
current = Path(__file__).resolve()
|
|
19
|
+
for parent in current.parents:
|
|
20
|
+
if (parent / ".git").exists() and (parent / "buildspec").exists():
|
|
21
|
+
return parent
|
|
22
|
+
cwd = Path.cwd().resolve()
|
|
23
|
+
for parent in (cwd, *cwd.parents):
|
|
24
|
+
if (parent / ".git").exists() and (parent / "buildspec").exists():
|
|
25
|
+
return parent
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _candidate_commands(repo_root: Path | None) -> list[list[str]]:
|
|
30
|
+
"""Return BuildSpec engine command candidates in preference order.
|
|
31
|
+
|
|
32
|
+
Order: an explicit ``BUILDSPEC_BIN`` override, then the engine vendored
|
|
33
|
+
in-repo at ``tools/buildspec/`` (the default for everyone after a plain
|
|
34
|
+
``git pull``), then a sibling ``../buildspec`` dev clone, then a ``buildspec``
|
|
35
|
+
binary on PATH.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
candidates: list[list[str]] = []
|
|
39
|
+
node = shutil.which("node")
|
|
40
|
+
|
|
41
|
+
env_bin = os.environ.get("BUILDSPEC_BIN")
|
|
42
|
+
if env_bin:
|
|
43
|
+
candidates.append([env_bin])
|
|
44
|
+
|
|
45
|
+
if repo_root is not None and node:
|
|
46
|
+
vendored = repo_root / "tools" / "buildspec" / "bin" / "buildspec.js"
|
|
47
|
+
if vendored.exists():
|
|
48
|
+
candidates.append([node, str(vendored)])
|
|
49
|
+
|
|
50
|
+
sibling = repo_root.parent / "buildspec" / "bin" / "buildspec.js"
|
|
51
|
+
if sibling.exists():
|
|
52
|
+
candidates.append([node, str(sibling)])
|
|
53
|
+
|
|
54
|
+
path_bin = shutil.which("buildspec")
|
|
55
|
+
if path_bin:
|
|
56
|
+
candidates.append([path_bin])
|
|
57
|
+
|
|
58
|
+
# De-duplicate while preserving order.
|
|
59
|
+
seen: set[tuple[str, ...]] = set()
|
|
60
|
+
unique: list[list[str]] = []
|
|
61
|
+
for cmd in candidates:
|
|
62
|
+
key = tuple(cmd)
|
|
63
|
+
if key not in seen:
|
|
64
|
+
seen.add(key)
|
|
65
|
+
unique.append(cmd)
|
|
66
|
+
return unique
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _resolve_buildspec(repo_root: Path | None) -> list[str]:
|
|
70
|
+
for cmd in _candidate_commands(repo_root):
|
|
71
|
+
exe = cmd[0]
|
|
72
|
+
if Path(exe).exists() or shutil.which(exe):
|
|
73
|
+
return cmd
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"BuildSpec engine not found. Set BUILDSPEC_BIN, install `buildspec`, "
|
|
76
|
+
"or clone build-ai-ego/buildspec next to the build-ai repo."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
CONTEXT_SETTINGS = {"allow_extra_args": True, "ignore_unknown_options": True}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run_buildspec(args: list[str]) -> int:
|
|
84
|
+
"""Run BuildSpec in the repo root with the provided arguments."""
|
|
85
|
+
|
|
86
|
+
repo_root = _repo_root()
|
|
87
|
+
if repo_root is None:
|
|
88
|
+
raise RuntimeError("Run `buildai spec` from the Build AI repo checkout.")
|
|
89
|
+
|
|
90
|
+
command = _resolve_buildspec(repo_root)
|
|
91
|
+
env = os.environ.copy()
|
|
92
|
+
env.setdefault("BUILDSPEC_PROJECT_ROOT", str(repo_root))
|
|
93
|
+
|
|
94
|
+
result = subprocess.run([*command, *args], cwd=repo_root, env=env)
|
|
95
|
+
return result.returncode
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def doctor() -> int:
|
|
99
|
+
"""Diagnose the BuildSpec engine integration: node, engine resolution,
|
|
100
|
+
project schema, and end-to-end change listing. Prints a report and returns
|
|
101
|
+
a non-zero exit code when anything is unhealthy."""
|
|
102
|
+
|
|
103
|
+
healthy = True
|
|
104
|
+
repo_root = _repo_root()
|
|
105
|
+
if repo_root is None:
|
|
106
|
+
error("Run `buildai spec doctor` from the Build AI repo checkout.")
|
|
107
|
+
return 1
|
|
108
|
+
|
|
109
|
+
node = shutil.which("node")
|
|
110
|
+
info(f"node: {node or 'NOT FOUND — install Node >= 20.19'}")
|
|
111
|
+
healthy = healthy and node is not None
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
command = _resolve_buildspec(repo_root)
|
|
115
|
+
info(f"engine: {' '.join(command)}")
|
|
116
|
+
except RuntimeError as exc:
|
|
117
|
+
error(f"engine: NOT RESOLVED — {exc}")
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
vendored_dir = repo_root / "tools" / "buildspec"
|
|
121
|
+
if str(vendored_dir) in " ".join(command):
|
|
122
|
+
source = vendored_dir / "SOURCE"
|
|
123
|
+
if source.exists():
|
|
124
|
+
info("vendored: " + source.read_text(encoding="utf-8").strip().replace("\n", "; "))
|
|
125
|
+
if not (vendored_dir / "node_modules").exists():
|
|
126
|
+
error("vendored engine deps missing — run `make buildspec-setup`")
|
|
127
|
+
healthy = False
|
|
128
|
+
|
|
129
|
+
env = os.environ.copy()
|
|
130
|
+
env.setdefault("BUILDSPEC_PROJECT_ROOT", str(repo_root))
|
|
131
|
+
|
|
132
|
+
schema = subprocess.run(
|
|
133
|
+
[*command, "schema", "which", "build-ai-change"],
|
|
134
|
+
cwd=repo_root,
|
|
135
|
+
env=env,
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
)
|
|
139
|
+
project = "Source: project" in (schema.stdout + schema.stderr)
|
|
140
|
+
info(f"schema build-ai-change: {'project ✓' if project else 'NOT project ✗'}")
|
|
141
|
+
healthy = healthy and project
|
|
142
|
+
|
|
143
|
+
listing = subprocess.run(
|
|
144
|
+
[*command, "list"], cwd=repo_root, env=env, capture_output=True, text=True
|
|
145
|
+
)
|
|
146
|
+
info(f"engine list: {'ok ✓' if listing.returncode == 0 else 'FAILED ✗'}")
|
|
147
|
+
healthy = healthy and listing.returncode == 0
|
|
148
|
+
|
|
149
|
+
info(f"buildai spec doctor: {'OK' if healthy else 'PROBLEMS'}")
|
|
150
|
+
return 0 if healthy else 1
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def spec_command(
|
|
154
|
+
ctx: typer.Context,
|
|
155
|
+
args: list[str] | None = typer.Argument(
|
|
156
|
+
None,
|
|
157
|
+
help="Arguments passed through to BuildSpec, e.g. `list --specs` or `validate <change> --strict`. Use `doctor` to diagnose the engine integration.",
|
|
158
|
+
),
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Run BuildSpec against this repo's `buildspec/` directory."""
|
|
161
|
+
|
|
162
|
+
forwarded = list(args or []) + list(ctx.args)
|
|
163
|
+
if forwarded and forwarded[0] == "doctor":
|
|
164
|
+
raise typer.Exit(code=doctor())
|
|
165
|
+
if not forwarded:
|
|
166
|
+
forwarded = ["--help"]
|
|
167
|
+
try:
|
|
168
|
+
code = run_buildspec(forwarded)
|
|
169
|
+
except RuntimeError as exc:
|
|
170
|
+
error(str(exc))
|
|
171
|
+
info("Try: BUILDSPEC_BIN=/path/to/buildspec buildai spec list")
|
|
172
|
+
raise typer.Exit(code=1) from exc
|
|
173
|
+
raise typer.Exit(code=code)
|
|
@@ -20,6 +20,8 @@ from cli.commands.dev import app as dev_app
|
|
|
20
20
|
from cli.commands.doctor import app as doctor_app
|
|
21
21
|
from cli.commands.egoexo import app as egoexo_app
|
|
22
22
|
from cli.commands.processing import app as processing_app
|
|
23
|
+
from cli.commands.spec import CONTEXT_SETTINGS as SPEC_CONTEXT_SETTINGS
|
|
24
|
+
from cli.commands.spec import spec_command
|
|
23
25
|
from cli.console import error, info, set_verbose, success, warning
|
|
24
26
|
|
|
25
27
|
|
|
@@ -274,6 +276,7 @@ app.add_typer(dev_app, name="dev")
|
|
|
274
276
|
app.add_typer(doctor_app, name="doctor")
|
|
275
277
|
app.add_typer(egoexo_app, name="egoexo")
|
|
276
278
|
app.add_typer(processing_app, name="processing")
|
|
279
|
+
app.command("spec", context_settings=SPEC_CONTEXT_SETTINGS)(spec_command)
|
|
277
280
|
|
|
278
281
|
|
|
279
282
|
def db_callback(
|
|
@@ -329,11 +332,13 @@ def _has_ingest_workspace() -> bool:
|
|
|
329
332
|
|
|
330
333
|
if has_core():
|
|
331
334
|
from cli.commands.db import app as db_app
|
|
335
|
+
from cli.commands.ego_frame_search import app as ego_frame_search_app
|
|
332
336
|
from cli.commands.gigcamera import app as gigcamera_app
|
|
333
337
|
from cli.commands.grid import app as grid_app
|
|
334
338
|
|
|
335
339
|
db_app.callback()(db_callback)
|
|
336
340
|
app.add_typer(db_app, name="db")
|
|
341
|
+
app.add_typer(ego_frame_search_app, name="ego-frame-search")
|
|
337
342
|
app.add_typer(gigcamera_app, name="gigcamera")
|
|
338
343
|
app.add_typer(grid_app, name="grid")
|
|
339
344
|
|
buildai_cli-0.3.87/AGENTS.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
CLAUDE.md
|
buildai_cli-0.3.87/CLAUDE.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# BuildAI CLI
|
|
2
|
-
|
|
3
|
-
Typer-based CLI with two modes: standalone (PyPI, API-backed) and workspace (repo-local, DB-direct).
|
|
4
|
-
|
|
5
|
-
## Two Planes
|
|
6
|
-
|
|
7
|
-
| Mode | Install | Auth | DB Access |
|
|
8
|
-
|------|---------|------|-----------|
|
|
9
|
-
| Standalone (`buildai`) | `uv tool install buildai-cli` | API key / JWT | Through API |
|
|
10
|
-
| Workspace (`uv run buildai`) | Repo-local editable install | IAM / password | Direct via `db`, through the profile-keyed local broker |
|
|
11
|
-
|
|
12
|
-
## Key Commands
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
buildai auth whoami # API auth inspection
|
|
16
|
-
uv run buildai ingest deploy plan --scope active # Dry-run ingest fleet plan
|
|
17
|
-
uv run buildai ingest deploy run --scope station:2 # Staged ingest rollout
|
|
18
|
-
uv run buildai ingest fleet readiness --scope active # Host readiness gate
|
|
19
|
-
buildai db query "SELECT count(*) FROM core.clips" # DB-direct
|
|
20
|
-
buildai db --env staging tunnel # IAP/SOCKS route for private staging DB
|
|
21
|
-
buildai db --env staging --all-proxy socks5://127.0.0.1:1080 query "SELECT 1" # staging query
|
|
22
|
-
buildai db broker status # Active local DB broker state
|
|
23
|
-
buildai db schema tables # Schema introspection
|
|
24
|
-
buildai db schema describe core.clips # Table details
|
|
25
|
-
buildai db --write migrate all # Run migrations
|
|
26
|
-
buildai db status # Migration status
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Guards
|
|
30
|
-
|
|
31
|
-
- `db` subcommands require workspace install + gcloud IAM.
|
|
32
|
-
- `db` inspection/migration commands use a profile-keyed `alloydb-auth-proxy`
|
|
33
|
-
broker owned by the local machine, with state in `~/.buildai/db-brokers` so
|
|
34
|
-
all worktrees reuse the same listener; use `buildai db broker status|ensure|stop`
|
|
35
|
-
for that local transport.
|
|
36
|
-
- Writes require `--write` flag.
|
|
37
|
-
- Production migrations prompt for confirmation.
|
|
38
|
-
- Worktree app/runtime targeting comes from explicit env vars and the repo Makefile, not a saved CLI context layer.
|
|
39
|
-
- `buildai db --env` selects the explicit DB lane: `production`, `staging`, `dev`.
|
|
40
|
-
- Staging is private-IP only. Start an IAP/VPN/SOCKS route first, then pass it
|
|
41
|
-
with `--all-proxy` so the local AlloyDB broker can reach the private address.
|
|
42
|
-
The standard route is `buildai db --env staging tunnel`.
|
|
43
|
-
|
|
44
|
-
## Reference
|
|
45
|
-
|
|
46
|
-
CLI mode guide: docs site Engineering section (run `make docs`)
|
|
47
|
-
Database roles: docs site Operations section (run `make docs`)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|