@odla-ai/cli 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -31
- package/REQUIREMENTS.md +18 -3
- package/dist/bin.cjs +1340 -436
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/chunk-OERLHVLH.js +2107 -0
- package/dist/chunk-OERLHVLH.js.map +1 -0
- package/dist/index.cjs +1353 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +314 -21
- package/dist/index.d.ts +314 -21
- package/dist/index.js +15 -1
- package/llms.txt +442 -54
- package/package.json +15 -6
- package/skills/odla/SKILL.md +43 -12
- package/skills/odla/references/build.md +65 -17
- package/skills/odla/references/sdks.md +36 -3
- package/skills/odla-migrate/SKILL.md +38 -13
- package/skills/odla-migrate/references/phase-0-preflight.md +0 -2
- package/skills/odla-migrate/references/phase-1-static.md +2 -4
- package/skills/odla-migrate/references/phase-2-db.md +25 -17
- package/skills/odla-migrate/references/phase-3-auth.md +106 -20
- package/skills/odla-migrate/references/phase-3b-user-sync.md +57 -0
- package/skills/odla-migrate/references/phase-4-ai.md +6 -5
- package/skills/odla-migrate/references/phase-5-cutover.md +47 -16
- package/skills/odla-migrate/references/secrets-map.md +26 -11
- package/skills/odla-migrate/references/troubleshooting.md +27 -13
- package/skills/odla-o11y-debug/SKILL.md +93 -0
- package/dist/chunk-UWT7C6VT.js +0 -1187
- package/dist/chunk-UWT7C6VT.js.map +0 -1
package/llms.txt
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
# @odla-ai/cli — LLM context
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Agent-operable CLI for odla provisioning, scoped System AI administration, hosted security runs, Worker secrets, and smoke checks.
|
|
4
4
|
|
|
5
|
-
> ⚠️ **
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
5
|
+
> ⚠️ **Early access — pre-1.0.** Agents work from bounded runbooks; humans
|
|
6
|
+
> approve credentials, production changes, releases, and merges. APIs and exact
|
|
7
|
+
> package availability can change. Review the documented guarantees and
|
|
8
|
+
> limitations; this software is MIT-licensed and provided without warranty.
|
|
9
9
|
|
|
10
10
|
Project-neutral provisioning CLI for odla apps. It creates and validates an
|
|
11
11
|
`odla.config.mjs`, then uses that config to register an app, enable services,
|
|
12
|
-
push odla-db schema/rules, configure
|
|
13
|
-
deployment links, and
|
|
12
|
+
push odla-db schema/rules, configure per-app BYOK AI, configure Clerk auth, record
|
|
13
|
+
deployment links, provision o11y ingest credentials, and transfer local or
|
|
14
|
+
deployed Worker secrets without printing them. It also gives platform admins a
|
|
15
|
+
scoped System AI control surface and gives app owners a provider-key-free
|
|
16
|
+
hosted security command.
|
|
14
17
|
|
|
15
18
|
It does not know about any specific app. App identity, environments, services,
|
|
16
19
|
schema, rules, auth, AI provider, and links all come from config.
|
|
@@ -28,7 +31,8 @@ For a project you keep working in, install it as a dev dependency so the
|
|
|
28
31
|
shorter `npx odla-ai` form works and the version is pinned by your lockfile:
|
|
29
32
|
|
|
30
33
|
```bash
|
|
31
|
-
npm
|
|
34
|
+
npm view @odla-ai/cli@0.6.0 version
|
|
35
|
+
npm i -D --save-exact @odla-ai/cli@0.6.0
|
|
32
36
|
```
|
|
33
37
|
|
|
34
38
|
## Prerequisites
|
|
@@ -48,12 +52,20 @@ npm i -D @odla-ai/cli
|
|
|
48
52
|
## Commands
|
|
49
53
|
|
|
50
54
|
```bash
|
|
55
|
+
npx odla-ai setup
|
|
51
56
|
npx odla-ai init --app-id my-app --name "My App"
|
|
52
57
|
npx odla-ai doctor
|
|
58
|
+
npx odla-ai capabilities --json
|
|
59
|
+
# install SDKs, write the Worker, and create wrangler.jsonc before secret push
|
|
53
60
|
npx odla-ai provision --dry-run
|
|
54
|
-
npx odla-ai provision --write-dev-vars
|
|
61
|
+
npx odla-ai provision --write-dev-vars --push-secrets
|
|
55
62
|
npx odla-ai smoke --env dev
|
|
56
63
|
npx odla-ai secrets push --env dev
|
|
64
|
+
npx odla-ai security run . --env dev --ack-redacted-source
|
|
65
|
+
npx odla-ai admin ai show
|
|
66
|
+
npx odla-ai admin ai set security.discovery --provider anthropic --model claude-opus-4-8 --enabled
|
|
67
|
+
npx odla-ai admin ai credentials
|
|
68
|
+
npx odla-ai admin ai credential set anthropic --from-env ANTHROPIC_API_KEY
|
|
57
69
|
npx odla-ai skill install
|
|
58
70
|
npx odla-ai version
|
|
59
71
|
```
|
|
@@ -65,8 +77,16 @@ npx odla-ai version
|
|
|
65
77
|
- `src/odla/rules.mjs`
|
|
66
78
|
- `.gitignore` entries for local credentials
|
|
67
79
|
|
|
80
|
+
New configs contain only the `dev` environment. Add `prod` explicitly after
|
|
81
|
+
the development flow is healthy; `provision` refuses to mutate `prod` or
|
|
82
|
+
`production` without `--yes` and supports `--dry-run` for review.
|
|
83
|
+
|
|
68
84
|
`provision` performs the standard safe setup flow:
|
|
69
85
|
|
|
86
|
+
When using `--push-secrets`, create the Worker and its Wrangler config first.
|
|
87
|
+
The CLI checks that config and `wrangler whoami` before it issues or rotates a
|
|
88
|
+
shown-once credential.
|
|
89
|
+
|
|
70
90
|
1. Gets an `odla_dev_...` token by device handshake, or reuses
|
|
71
91
|
`ODLA_DEV_TOKEN` / `.odla/dev-token.json`.
|
|
72
92
|
In an interactive terminal it opens the approval page in your browser when
|
|
@@ -75,14 +95,28 @@ npx odla-ai version
|
|
|
75
95
|
best-effort; the printed URL and code always remain the fallback.
|
|
76
96
|
2. Creates the platform app if needed.
|
|
77
97
|
3. Enables configured services in every configured environment.
|
|
78
|
-
4. Mints or reuses each env's odla-db app
|
|
98
|
+
4. Mints or reuses each env's configured service credentials: an odla-db app
|
|
99
|
+
key for `db` and an ingest token for `o11y`.
|
|
79
100
|
5. Pushes the configured schema and rules.
|
|
80
101
|
6. Configures platform AI and stores provider keys in the tenant vault when the
|
|
81
102
|
configured key env var is set.
|
|
82
103
|
7. Writes `.odla/credentials.local.json` with mode `0600`.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
8. With `--write-dev-vars`, writes the local Worker values to `.dev.vars`; with
|
|
105
|
+
`--push-secrets`, transfers the configured service secrets to the matching
|
|
106
|
+
deployed Worker environment over Wrangler stdin.
|
|
107
|
+
|
|
108
|
+
Plain reruns are idempotent. Pass `--rotate-o11y-token` only when deliberately
|
|
109
|
+
invalidating and replacing the o11y token; combine it with `--push-secrets` so
|
|
110
|
+
the replacement reaches the Worker in the same run. `--rotate-keys` remains the
|
|
111
|
+
broader credential rotation. Production mutation or secret transfer requires
|
|
112
|
+
`--yes`. Pass `--no-write-credentials` to skip writing the local credentials
|
|
113
|
+
file; it cannot be combined with an operation that would issue a shown-once
|
|
114
|
+
credential.
|
|
115
|
+
|
|
116
|
+
Rotation and Wrangler secret replacement are not a distributed transaction.
|
|
117
|
+
The CLI preflights Wrangler first and persists the replacement before pushing;
|
|
118
|
+
if that final push still fails, follow the printed `secrets push --env ...`
|
|
119
|
+
retry. Do not run the rotation flag again.
|
|
86
120
|
|
|
87
121
|
`smoke` verifies a provisioned environment from local credentials. It fetches
|
|
88
122
|
the platform public config, checks the configured AI provider, fetches the live
|
|
@@ -90,23 +124,82 @@ odla-db schema with the tenant key, compares expected schema entities, and runs
|
|
|
90
124
|
a count aggregate against the first entity. It fails early with a clear message
|
|
91
125
|
when provisioning has not written `.odla/credentials.local.json`.
|
|
92
126
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
The bundled build/migration skills add a passive pre-ship gate with
|
|
128
|
+
`@odla-ai/security`: after the exact-version availability check, run
|
|
129
|
+
`npm i -D --save-exact @odla-ai/security@0.2.0`, then
|
|
130
|
+
`npx odla-security scan . --profile odla --out .odla/security/pre-ship --fail-on high --fail-on-candidates critical`.
|
|
131
|
+
The passive scan remains a separate binary: it makes no model calls and does
|
|
132
|
+
not execute target code. After explicit redacted-source approval,
|
|
133
|
+
`odla-ai security run . --env dev --ack-redacted-source` adds hosted discovery
|
|
134
|
+
and independent validation. The CLI obtains/reuses app-owner auth and the
|
|
135
|
+
platform supplies bounded role grants; it never requests provider keys.
|
|
136
|
+
Early-access run ceilings are rolling; a `429` includes the platform's safe
|
|
137
|
+
retry hint, which the CLI surfaces without echoing arbitrary response text.
|
|
138
|
+
Before installing the exact release, require
|
|
139
|
+
`npm view @odla-ai/security@0.2.0 version` to succeed before installation. An
|
|
140
|
+
exact-version `E404` means the release is unavailable, not that the security
|
|
141
|
+
preflight passed, and does not prove the package name is absent. odla's own
|
|
142
|
+
engineering environment uses an equivalent internal gate; customer projects
|
|
143
|
+
should use the published `odla-security` command above.
|
|
144
|
+
|
|
145
|
+
`admin ai show|set|credentials|credential set` manages platform-funded System
|
|
146
|
+
AI (`o11y.triage`, `security.discovery`, `security.validation`). It uses a
|
|
147
|
+
separate, mode-`0600` `.odla/admin-token.local.json` cache. Studio displays the
|
|
148
|
+
exact scope, only an admin can approve it, and it expires after ~15 minutes;
|
|
149
|
+
the token never becomes ambient admin. Credential writes accept only
|
|
150
|
+
`--from-env <NAME>` or `--stdin` and never place the value in argv, output, or
|
|
151
|
+
the cache. System AI and each app's BYOK AI are separate vault/funding paths.
|
|
152
|
+
|
|
153
|
+
`secrets push --env <env>` is the narrower recovery/retry command. It moves the
|
|
154
|
+
configured env's odla-db key and/or o11y ingest token from
|
|
155
|
+
`.odla/credentials.local.json` into the deployed Worker by piping each value
|
|
156
|
+
over stdin to `wrangler secret put`; values never appear on argv, in output, or
|
|
157
|
+
in an agent's transcript. It preflights `wrangler whoami` and the presence of a
|
|
158
|
+
wrangler config file. The env `prod`/`production` targets the top-level
|
|
159
|
+
wrangler environment (no `--env` flag is passed to Wrangler) and requires
|
|
160
|
+
`--yes`; every other env maps to `wrangler --env <name>`. `--dry-run` prints a
|
|
161
|
+
redacted plan without spawning anything. For the normal path, use
|
|
162
|
+
`provision --write-dev-vars --push-secrets` so service enablement, credential
|
|
163
|
+
issuance, local config, and deployed secret transfer stay one composition.
|
|
164
|
+
|
|
165
|
+
## Who does what
|
|
166
|
+
|
|
167
|
+
If a change is deterministic from `odla.config.mjs`, the CLI owns it: app and
|
|
168
|
+
service enablement, credential issuance and local persistence, `.dev.vars`,
|
|
169
|
+
schema/rules/auth/AI configuration, and Wrangler secret transfer. The coding
|
|
170
|
+
agent owns application semantics: installing `@odla-ai/o11y`, wrapping the
|
|
171
|
+
Worker with `withObservability`, and choosing useful spans, metrics, errors, and
|
|
172
|
+
LLM-usage records. The human owns the device approval, production consent, and
|
|
173
|
+
explicit destructive rotation. Studio is where people view telemetry and
|
|
174
|
+
perform manual recovery, configure System AI, and inspect app/run-attributed
|
|
175
|
+
platform usage; its o11y token control is not the normal setup path.
|
|
176
|
+
Run `npx odla-ai capabilities` for the human-readable contract or add `--json`
|
|
177
|
+
when an agent or tool needs to branch on it without scraping prose.
|
|
178
|
+
|
|
179
|
+
`setup` (and `skill install`) installs one complete offline runbook bundle at
|
|
180
|
+
`.agents/skills/`, then adds thin native adapters for Claude Code
|
|
181
|
+
(`.claude/skills/`), Codex, Cursor (`.cursor/rules/`), GitHub Copilot
|
|
182
|
+
(`.github/copilot-instructions.md`), Gemini CLI (`GEMINI.md`), and portable
|
|
183
|
+
`AGENTS.md` agents. The default is `--agent all`; select one or more with
|
|
184
|
+
`--agent claude,codex` or repeated `--agent` flags. A global install supports
|
|
185
|
+
the native Claude and Codex roots only.
|
|
186
|
+
|
|
187
|
+
Existing `AGENTS.md`, `GEMINI.md`, and Copilot instructions are preserved. The
|
|
188
|
+
installer appends a bounded odla-managed section, is idempotent, refuses
|
|
189
|
+
symlink-redirection and locally changed managed content, and lets `--force`
|
|
190
|
+
replace only odla-owned content. The local bundle includes build, migration,
|
|
191
|
+
and observability-debug procedures plus every reference they need; agents do
|
|
192
|
+
not need online docs or access to odla's private service source after setup.
|
|
193
|
+
|
|
194
|
+
After running setup, the whole prompt can be: “Set up odla in this repo using
|
|
195
|
+
the installed local odla runbook. Decide whether this is a new app or an
|
|
196
|
+
existing site to migrate, then drive it end to end. Ask me only for the device
|
|
197
|
+
approval and other human-only checkpoints.”
|
|
198
|
+
|
|
199
|
+
For programmatic callers, `installSkill()` keeps `targetDir`, `written`, and
|
|
200
|
+
`unchanged` as one unambiguous primary-root view: the latter two contain paths
|
|
201
|
+
relative to `targetDir` only. Use the additive absolute `writtenPaths` and
|
|
202
|
+
`unchangedPaths` fields to inspect every shared-bundle and adapter artifact.
|
|
110
203
|
|
|
111
204
|
`doctor` also lints for common footguns: permission rules that are literally
|
|
112
205
|
`"true"` (public writes always warn; a public `view` warns unless the
|
|
@@ -124,8 +217,8 @@ export default {
|
|
|
124
217
|
platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
|
|
125
218
|
dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
|
|
126
219
|
app: { id: "my-app", name: "My App" },
|
|
127
|
-
envs: ["
|
|
128
|
-
services: ["db", "ai"],
|
|
220
|
+
envs: ["dev"], // add "prod" deliberately; provisioning it requires --yes
|
|
221
|
+
services: ["db", "ai", "o11y"],
|
|
129
222
|
db: {
|
|
130
223
|
schema: "./src/odla/schema.mjs",
|
|
131
224
|
rules: "./src/odla/rules.mjs",
|
|
@@ -137,6 +230,8 @@ export default {
|
|
|
137
230
|
keyEnv: "ANTHROPIC_API_KEY",
|
|
138
231
|
},
|
|
139
232
|
auth: {
|
|
233
|
+
// Publishable key (public). Created + pulled by the Clerk CLI (`npx clerk`);
|
|
234
|
+
// See the installed odla-migrate phase 3. Inline `pk_…` or "$ENV_VAR".
|
|
140
235
|
clerk: {
|
|
141
236
|
dev: "$CLERK_PUBLISHABLE_KEY",
|
|
142
237
|
prod: "$CLERK_PUBLISHABLE_KEY",
|
|
@@ -156,15 +251,123 @@ If schema is present and rules are omitted, `defaultRules: "deny"` generates
|
|
|
156
251
|
deny-all rules for every schema entity. That is the safe default for Workers
|
|
157
252
|
that mediate reads and writes with an app key.
|
|
158
253
|
|
|
159
|
-
## API reference (generated from dist/index.d.ts, v0.
|
|
254
|
+
## API reference (generated from dist/index.d.ts, v0.6.0)
|
|
160
255
|
|
|
161
256
|
```ts
|
|
257
|
+
import { SecurityReport, PlatformSecurityRun } from '@odla-ai/security';
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Parse CLI arguments and dispatch to the matching command — the entry point
|
|
261
|
+
* `bin.ts` invokes. `argv` defaults to `process.argv.slice(2)`; the first
|
|
262
|
+
* positional selects the command and the rest are parsed into flags (supporting
|
|
263
|
+
* `--flag value`, `--flag=value`, boolean `--flag`, `--no-flag`, and repeated
|
|
264
|
+
* flags collected into arrays).
|
|
265
|
+
*
|
|
266
|
+
* Recognized commands: `version`/`-v`/`--version` and `help`/`-h`/`--help` print
|
|
267
|
+
* and return; `init` scaffolds a project (requires `--app-id` and `--name`);
|
|
268
|
+
* `doctor` validates config offline; `capabilities` prints the responsibility
|
|
269
|
+
* boundary; `admin ai` reads/updates purpose-specific platform AI policies and
|
|
270
|
+
* write-only credentials through a short-lived exact-scope device grant;
|
|
271
|
+
* `security run` performs an explicitly acknowledged, app-attributed hosted
|
|
272
|
+
* reasoning pass; `provision` runs the full provision (including optional secret
|
|
273
|
+
* rotation, local vars, and deployed secret push); `smoke` checks a live env;
|
|
274
|
+
* `setup` and `skill install` install the bundled offline runbooks for Claude
|
|
275
|
+
* Code, Codex, Cursor, Copilot, Gemini, and `AGENTS.md`-aware agents; and
|
|
276
|
+
* `secrets push` pipes the env's configured db/o11y secrets into the Worker via
|
|
277
|
+
* Wrangler (requires `--env`). Throws on an unknown command/subcommand, unsupported option, extra
|
|
278
|
+
* positional argument, missing required value, or a dispatched command error.
|
|
279
|
+
*
|
|
280
|
+
* @param argv Argument vector without the node/script prefix (default
|
|
281
|
+
* `process.argv.slice(2)`).
|
|
282
|
+
*/
|
|
162
283
|
declare function runCli(argv?: string[]): Promise<void>;
|
|
163
284
|
|
|
285
|
+
/** Hosted platform purposes whose provider, model, and budgets an admin may route. */
|
|
286
|
+
declare const SYSTEM_AI_PURPOSES: readonly ["o11y.triage", "security.discovery", "security.validation"];
|
|
287
|
+
/** One catalog-validated, platform-funded System AI purpose. */
|
|
288
|
+
type SystemAiPurpose = (typeof SYSTEM_AI_PURPOSES)[number];
|
|
289
|
+
/** Inputs for one System AI read, policy update, credential-status read, or
|
|
290
|
+
* write-only credential transfer. Provider values may come only from a named
|
|
291
|
+
* environment variable or stdin. */
|
|
292
|
+
interface AdminAiOptions {
|
|
293
|
+
action: "show" | "set" | "credentials" | "credential-set";
|
|
294
|
+
purpose?: string;
|
|
295
|
+
provider?: string;
|
|
296
|
+
model?: string;
|
|
297
|
+
enabled?: boolean;
|
|
298
|
+
maxInputBytes?: number;
|
|
299
|
+
maxOutputTokens?: number;
|
|
300
|
+
maxCallsPerRun?: number;
|
|
301
|
+
platform?: string;
|
|
302
|
+
token?: string;
|
|
303
|
+
json?: boolean;
|
|
304
|
+
open?: boolean;
|
|
305
|
+
fetch?: typeof fetch;
|
|
306
|
+
stdout?: Pick<typeof console, "log">;
|
|
307
|
+
openApprovalUrl?: (url: string) => Promise<void>;
|
|
308
|
+
credentialProvider?: string;
|
|
309
|
+
fromEnv?: string;
|
|
310
|
+
stdin?: boolean;
|
|
311
|
+
readStdin?: () => Promise<string>;
|
|
312
|
+
/** Separate short-lived admin-token cache; never reuses the app developer-token file. */
|
|
313
|
+
tokenFile?: string;
|
|
314
|
+
}
|
|
315
|
+
/** Inputs for obtaining one short-lived, exact-scope platform device grant. */
|
|
316
|
+
interface ScopedPlatformTokenOptions {
|
|
317
|
+
platform: string;
|
|
318
|
+
scope: "platform:ai:read" | "platform:ai:write" | "platform:security:self";
|
|
319
|
+
open?: boolean;
|
|
320
|
+
fetch?: typeof fetch;
|
|
321
|
+
stdout?: Pick<typeof console, "log">;
|
|
322
|
+
openApprovalUrl?: (url: string) => Promise<void>;
|
|
323
|
+
tokenFile?: string;
|
|
324
|
+
}
|
|
325
|
+
/** Obtain/cache one exact platform scope without granting ambient admin. */
|
|
326
|
+
declare function getScopedPlatformToken(options: ScopedPlatformTokenOptions): Promise<string>;
|
|
327
|
+
/** Read or update platform-funded AI routing. Authentication is a short-lived,
|
|
328
|
+
* explicitly scoped device grant; ordinary app developer tokens never become
|
|
329
|
+
* platform admins. Credential writes accept values only from a named env var
|
|
330
|
+
* or stdin, transfer them once, and never place them in argv, output, or the
|
|
331
|
+
* scoped-token cache. */
|
|
332
|
+
declare function adminAi(options: AdminAiOptions): Promise<void>;
|
|
333
|
+
|
|
334
|
+
/** Stable, machine-readable division of work between the CLI, coding agent,
|
|
335
|
+
* human operator, and Studio. Printed by `odla-ai capabilities --json`. */
|
|
336
|
+
declare const CAPABILITIES: {
|
|
337
|
+
readonly cli: readonly ["register the app and enable configured services per environment", "issue, persist, and explicitly rotate configured service credentials", "write local Worker values and push deployed Worker secrets through Wrangler stdin", "push db schema/rules and configure platform AI, auth, and deployment links", "validate config offline and smoke-test a provisioned db environment", "run app-attributed hosted security discovery and independent validation without provider keys", "let admins manage system AI routes/credentials through short-lived exact-scope approval"];
|
|
338
|
+
readonly agent: readonly ["install and import the selected odla SDKs", "wrap the Worker with withObservability and choose useful telemetry", "make application-specific schema, rules, auth, UI, and migration decisions"];
|
|
339
|
+
readonly human: readonly ["approve the odla device code and one-off third-party logins", "consent to production changes with --yes", "request destructive credential rotation explicitly", "review application semantics, security findings, releases, and merges", "approve redacted-source disclosure before hosted security reasoning"];
|
|
340
|
+
readonly studio: readonly ["view telemetry and environment state", "perform manual credential recovery when the CLI's local shown-once copy is unavailable", "configure system AI purposes and view app/environment/run-attributed usage"];
|
|
341
|
+
};
|
|
342
|
+
type Output = Pick<typeof console, "log">;
|
|
343
|
+
/** Print the stable responsibility boundary for humans or agent tooling. */
|
|
344
|
+
declare function printCapabilities(json?: boolean, out?: Output): void;
|
|
345
|
+
|
|
164
346
|
interface DoctorOptions {
|
|
165
347
|
configPath: string;
|
|
166
348
|
stdout?: Pick<typeof console, "log" | "error">;
|
|
167
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Validate and summarize an odla project config entirely offline — no network
|
|
352
|
+
* calls and no file writes. Loads `configPath`, builds the provision plan, and
|
|
353
|
+
* resolves the schema and rules (synthesizing deny-all rules from the schema
|
|
354
|
+
* when `db.rules` is omitted and `db.defaultRules` isn't `false`), then prints a
|
|
355
|
+
* summary line for the app, envs, services, schema entity count, rules namespace
|
|
356
|
+
* count, and AI provider.
|
|
357
|
+
*
|
|
358
|
+
* It then collects warnings for the mistakes provision can't fix silently: a
|
|
359
|
+
* schema with no entities, a rules namespace with no matching entity, the `ai`
|
|
360
|
+
* service enabled without an `ai.provider`, `auth.clerk` entries that reference
|
|
361
|
+
* an unset env var (`$FOO`), an `ai.keyEnv` that isn't set (provider key upload
|
|
362
|
+
* will be skipped), and — when `o11y` is enabled — any env whose local
|
|
363
|
+
* credentials lack an ingest token. It also folds in the shared rule linter,
|
|
364
|
+
* git-tracked secret-shaped files, and wrangler config warnings. Prints `ok`
|
|
365
|
+
* when clean, otherwise the warning list. Purely advisory: never throws on
|
|
366
|
+
* warnings and never mutates anything.
|
|
367
|
+
*
|
|
368
|
+
* @param options.configPath Path to the `odla.config.mjs` to inspect.
|
|
369
|
+
* @param options.stdout Optional console-like sink (defaults to `console`).
|
|
370
|
+
*/
|
|
168
371
|
declare function doctor(options: DoctorOptions): Promise<void>;
|
|
169
372
|
|
|
170
373
|
interface InitOptions {
|
|
@@ -178,8 +381,44 @@ interface InitOptions {
|
|
|
178
381
|
force?: boolean;
|
|
179
382
|
stdout?: Pick<typeof console, "log" | "error">;
|
|
180
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Scaffold a new odla project into `rootDir` (default: cwd). Writes an
|
|
386
|
+
* `odla.config.mjs` (default name, overridable via `configPath`) plus starter
|
|
387
|
+
* `src/odla/schema.mjs` (a single `notes` entity) and `src/odla/rules.mjs`
|
|
388
|
+
* (deny-all), and creates the `src/odla` and `.odla` directories. Also runs
|
|
389
|
+
* `ensureGitignore` so the local token/credentials paths are ignored.
|
|
390
|
+
*
|
|
391
|
+
* Fully local and synchronous — no network calls. The config file is only
|
|
392
|
+
* written when it doesn't already exist unless `force` is set (otherwise it
|
|
393
|
+
* throws); the schema/rules files are written only when missing (never
|
|
394
|
+
* overwritten, even with `force`). `appId` must match `^[a-z0-9][a-z0-9-]*$` or
|
|
395
|
+
* it throws. Defaults: `envs` → `["dev"]`, `services` → `["db","ai"]`,
|
|
396
|
+
* `aiProvider` → `"anthropic"` (which also picks the `keyEnv`, e.g.
|
|
397
|
+
* `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`/`GOOGLE_API_KEY`).
|
|
398
|
+
*
|
|
399
|
+
* @param options.appId Slug for the app (lowercase alphanumerics and hyphens).
|
|
400
|
+
* @param options.name Human-readable app name embedded in the config.
|
|
401
|
+
* @param options.rootDir Project root (defaults to `process.cwd()`).
|
|
402
|
+
* @param options.configPath Config filename relative to root (default `odla.config.mjs`).
|
|
403
|
+
* @param options.envs Environment names (default `["dev"]`; production is an explicit opt-in).
|
|
404
|
+
* @param options.services Enabled services (default `["db","ai"]`).
|
|
405
|
+
* @param options.aiProvider AI provider slug (default `"anthropic"`).
|
|
406
|
+
* @param options.force Overwrite an existing config file instead of throwing.
|
|
407
|
+
* @param options.stdout Optional console-like sink (defaults to `console`).
|
|
408
|
+
*/
|
|
181
409
|
declare function initProject(options: InitOptions): void;
|
|
182
410
|
|
|
411
|
+
interface RunResult {
|
|
412
|
+
code: number;
|
|
413
|
+
stdout: string;
|
|
414
|
+
stderr: string;
|
|
415
|
+
}
|
|
416
|
+
/** Subprocess seam: injectable in tests, `defaultRunner` in production. */
|
|
417
|
+
type CommandRunner = (cmd: string, args: string[], opts?: {
|
|
418
|
+
input?: string;
|
|
419
|
+
cwd?: string;
|
|
420
|
+
}) => Promise<RunResult>;
|
|
421
|
+
|
|
183
422
|
type OdlaEnvName = string;
|
|
184
423
|
type AppRules = Record<string, {
|
|
185
424
|
view?: string;
|
|
@@ -192,6 +431,7 @@ interface ClerkAuthConfig {
|
|
|
192
431
|
audience?: string;
|
|
193
432
|
mode?: "client" | "full";
|
|
194
433
|
}
|
|
434
|
+
/** Authored `odla.config.mjs` contract consumed by doctor and provision. */
|
|
195
435
|
interface OdlaProjectConfig {
|
|
196
436
|
platformUrl?: string;
|
|
197
437
|
dbEndpoint?: string;
|
|
@@ -238,6 +478,7 @@ interface OdlaProjectConfig {
|
|
|
238
478
|
gitignore?: boolean;
|
|
239
479
|
};
|
|
240
480
|
}
|
|
481
|
+
/** Normalized project config with resolved paths, endpoints, envs, and services. */
|
|
241
482
|
interface LoadedProjectConfig extends OdlaProjectConfig {
|
|
242
483
|
configPath: string;
|
|
243
484
|
rootDir: string;
|
|
@@ -252,6 +493,7 @@ interface LoadedProjectConfig extends OdlaProjectConfig {
|
|
|
252
493
|
gitignore: boolean;
|
|
253
494
|
};
|
|
254
495
|
}
|
|
496
|
+
/** Private mode-0600 local state written by provision and never committed. */
|
|
255
497
|
interface LocalCredentials {
|
|
256
498
|
appId: string;
|
|
257
499
|
platformUrl: string;
|
|
@@ -263,10 +505,15 @@ interface LocalCredentials {
|
|
|
263
505
|
o11yToken?: string;
|
|
264
506
|
}>;
|
|
265
507
|
}
|
|
508
|
+
/** Controls one dev-first provisioning run; production mutation requires `yes`. */
|
|
266
509
|
interface ProvisionOptions {
|
|
267
510
|
configPath: string;
|
|
268
511
|
dryRun?: boolean;
|
|
269
512
|
rotateKeys?: boolean;
|
|
513
|
+
/** Rotate only the o11y ingest token; leaves db keys unchanged. */
|
|
514
|
+
rotateO11yToken?: boolean;
|
|
515
|
+
/** Push configured Worker secrets to every configured env after provisioning. */
|
|
516
|
+
pushSecrets?: boolean;
|
|
270
517
|
writeCredentials?: boolean;
|
|
271
518
|
writeDevVars?: string | boolean;
|
|
272
519
|
token?: string;
|
|
@@ -276,10 +523,14 @@ interface ProvisionOptions {
|
|
|
276
523
|
interactive?: boolean;
|
|
277
524
|
/** Test/embedding override for opening the approval URL. Defaults to the OS browser opener. */
|
|
278
525
|
openApprovalUrl?: (url: string) => Promise<void>;
|
|
526
|
+
/** Explicit consent for a non-dry-run plan containing `prod` or `production`. */
|
|
279
527
|
yes?: boolean;
|
|
280
528
|
fetch?: typeof fetch;
|
|
529
|
+
/** Test/embedding override for Wrangler subprocesses used by `pushSecrets`. */
|
|
530
|
+
secretRunner?: CommandRunner;
|
|
281
531
|
stdout?: Pick<typeof console, "log" | "error">;
|
|
282
532
|
}
|
|
533
|
+
/** Human-reviewable, secret-free plan printed by `provision({ dryRun: true })`. */
|
|
283
534
|
interface ProvisionPlan {
|
|
284
535
|
appId: string;
|
|
285
536
|
appName: string;
|
|
@@ -291,6 +542,7 @@ interface ProvisionPlan {
|
|
|
291
542
|
hasRules: boolean;
|
|
292
543
|
aiProvider?: string;
|
|
293
544
|
}
|
|
545
|
+
/** Inputs for a read-only live environment smoke check. */
|
|
294
546
|
interface SmokeOptions {
|
|
295
547
|
configPath: string;
|
|
296
548
|
env?: string;
|
|
@@ -298,21 +550,50 @@ interface SmokeOptions {
|
|
|
298
550
|
stdout?: Pick<typeof console, "log" | "error">;
|
|
299
551
|
}
|
|
300
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Provision an app end-to-end from its config — the primary deploy path, and
|
|
555
|
+
* (apart from key rotation) idempotent, so re-running is safe. Obtains a
|
|
556
|
+
* developer token, creates the registry app when absent, enables each service
|
|
557
|
+
* per env (AI also gets `setAi` with the configured provider/model), configures
|
|
558
|
+
* Clerk auth and links, then per env mints or reuses credentials only for the
|
|
559
|
+
* configured services, pushes the schema with the db key and the
|
|
560
|
+
* rules with the developer token, and stores the provider key in the tenant
|
|
561
|
+
* vault when `ai.keyEnv` is set and present. Finally writes the credentials file
|
|
562
|
+
* (mode 0600, gitignored) and, when requested, `.dev.vars`.
|
|
563
|
+
*
|
|
564
|
+
* The developer token comes from (in order) `options.token`, `$ODLA_DEV_TOKEN`,
|
|
565
|
+
* the cached `.odla` token, or a fresh device handshake — which prints a user
|
|
566
|
+
* code + approval URL and, in interactive terminals, auto-opens the browser
|
|
567
|
+
* (`open: true`/`false` forces/suppresses it). Network error bodies pass through
|
|
568
|
+
* `redactSecrets` before display.
|
|
569
|
+
*
|
|
570
|
+
* @param options.dryRun Print the resolved plan and return before any network call/write.
|
|
571
|
+
* @param options.rotateKeys Rotate db keys and o11y tokens.
|
|
572
|
+
* @param options.rotateO11yToken Rotate only the o11y token.
|
|
573
|
+
* @param options.pushSecrets Preflight Wrangler, then push each env's configured
|
|
574
|
+
* Worker secrets immediately after credential provisioning.
|
|
575
|
+
* @param options.writeDevVars `true`/a path writes `.dev.vars`; `writeCredentials: false` skips the creds file.
|
|
576
|
+
* @param options.yes Required for a non-dry-run plan containing `prod` or
|
|
577
|
+
* `production`; callers must show and review `dryRun` output first.
|
|
578
|
+
* @throws When a production plan is executed without `yes`, configuration is
|
|
579
|
+
* invalid, authorization fails, or a provisioning operation fails.
|
|
580
|
+
*/
|
|
301
581
|
declare function provision(options: ProvisionOptions): Promise<void>;
|
|
302
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Return `value` with every secret-shaped substring replaced by a `[redacted]`
|
|
585
|
+
* placeholder that keeps the identifying prefix (e.g. `sk_live_[redacted]`).
|
|
586
|
+
* Matches odla db keys (`odla_sk_`/`odla_dev_`), Clerk secret keys
|
|
587
|
+
* (`sk_live_`/`sk_test_`), OpenAI keys (`sk-`), webhook signing secrets
|
|
588
|
+
* (`whsec_`), o11y tokens (`o11y_`), GitHub tokens (`ghp_`/`gho_`/`github_pat_`),
|
|
589
|
+
* and AWS access-key ids (`AKIA…`). Clerk publishable keys (`pk_test_`/
|
|
590
|
+
* `pk_live_`) are public by design and intentionally left untouched. Used to
|
|
591
|
+
* sanitize anything the CLI echoes (notably network error bodies) before it
|
|
592
|
+
* reaches stdout/stderr. Non-matching input is returned unchanged.
|
|
593
|
+
*/
|
|
303
594
|
declare function redactSecrets(value: string): string;
|
|
304
595
|
|
|
305
|
-
|
|
306
|
-
code: number;
|
|
307
|
-
stdout: string;
|
|
308
|
-
stderr: string;
|
|
309
|
-
}
|
|
310
|
-
/** Subprocess seam: injectable in tests, `defaultRunner` in production. */
|
|
311
|
-
type CommandRunner = (cmd: string, args: string[], opts?: {
|
|
312
|
-
input?: string;
|
|
313
|
-
cwd?: string;
|
|
314
|
-
}) => Promise<RunResult>;
|
|
315
|
-
|
|
596
|
+
/** Moves locally stored configured service credentials to one Wrangler environment over stdin. */
|
|
316
597
|
interface SecretsPushOptions {
|
|
317
598
|
configPath: string;
|
|
318
599
|
env: string;
|
|
@@ -323,38 +604,145 @@ interface SecretsPushOptions {
|
|
|
323
604
|
stdout?: Pick<typeof console, "log" | "error">;
|
|
324
605
|
}
|
|
325
606
|
/**
|
|
326
|
-
* Moves the env's
|
|
327
|
-
*
|
|
328
|
-
*
|
|
607
|
+
* Moves the env's configured db and/or o11y credentials from the private local
|
|
608
|
+
* credentials file into the Worker, piping values over stdin so they never
|
|
609
|
+
* appear on argv, in output, or in the conversation transcript.
|
|
329
610
|
*/
|
|
330
611
|
declare function secretsPush(options: SecretsPushOptions): Promise<void>;
|
|
331
612
|
|
|
613
|
+
/** Built-in security coverage profile selected for a hosted reasoning run. */
|
|
614
|
+
type HostedSecurityProfile = "odla" | "cloudflare-app" | "generic";
|
|
615
|
+
/** Exact attribution and authorization need supplied to a lazy token callback. */
|
|
616
|
+
interface HostedSecurityTokenRequest {
|
|
617
|
+
platform: string;
|
|
618
|
+
appId: string;
|
|
619
|
+
env: string;
|
|
620
|
+
selfAudit: boolean;
|
|
621
|
+
/** Self-audit asks the caller for an explicitly scoped platform credential. */
|
|
622
|
+
/** Ordinary app runs use owner authority and therefore request no platform scope. */
|
|
623
|
+
scope: "platform:security:self" | null;
|
|
624
|
+
}
|
|
625
|
+
/** Local snapshot/orchestration controls for an app-attributed hosted security
|
|
626
|
+
* run. The platform supplies model routes and provider credentials. */
|
|
627
|
+
interface RunHostedSecurityOptions {
|
|
628
|
+
/** Project config; loaded for customer-app runs and ignored for self-audit. */
|
|
629
|
+
configPath?: string;
|
|
630
|
+
/** Scan odla itself under the protected odla-ai/prod identity. */
|
|
631
|
+
selfAudit?: boolean;
|
|
632
|
+
/** Repository directory to snapshot. Defaults to the config root or cwd for self-audit. */
|
|
633
|
+
target?: string;
|
|
634
|
+
/** Private artifact directory. Defaults to `<target>/.odla/security/hosted`. */
|
|
635
|
+
out?: string;
|
|
636
|
+
/** Customer environment. Must be declared in config. Self-audit always uses prod. */
|
|
637
|
+
env?: string;
|
|
638
|
+
profile?: HostedSecurityProfile;
|
|
639
|
+
maxHuntTasks?: number;
|
|
640
|
+
/** Exact acknowledgement required before any source capture or network call. */
|
|
641
|
+
sourceDisclosureAck?: "redacted";
|
|
642
|
+
/** Optional client correlation id; the platform returns the authoritative run id. */
|
|
643
|
+
runId?: string;
|
|
644
|
+
/** Platform override. Customer runs otherwise use config.platformUrl. */
|
|
645
|
+
platform?: string;
|
|
646
|
+
/** Injected app credential; for self-audit this must be explicitly scoped by the caller. */
|
|
647
|
+
token?: string;
|
|
648
|
+
/** Lazy token acquisition. Receives the exact app/env and required scope. */
|
|
649
|
+
getToken?: (request: HostedSecurityTokenRequest) => string | Promise<string>;
|
|
650
|
+
fetch?: typeof fetch;
|
|
651
|
+
signal?: AbortSignal;
|
|
652
|
+
stdout?: Pick<typeof console, "log" | "error">;
|
|
653
|
+
}
|
|
654
|
+
/** Completed local report, public platform run metadata, and private artifact path. */
|
|
655
|
+
interface HostedSecurityResult {
|
|
656
|
+
report: SecurityReport;
|
|
657
|
+
run: Readonly<PlatformSecurityRun>;
|
|
658
|
+
output: string;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Run the hosted, redacted-source security harness for an odla app or odla's
|
|
662
|
+
* protected self-audit identity. Provider/model selection and provider secrets
|
|
663
|
+
* remain platform-owned; this function accepts only an app/scoped bearer and
|
|
664
|
+
* the opaque role grants remain inside @odla-ai/security's reasoner closures.
|
|
665
|
+
*/
|
|
666
|
+
declare function runHostedSecurity(options: RunHostedSecurityOptions): Promise<HostedSecurityResult>;
|
|
667
|
+
|
|
668
|
+
/** Agent harnesses with a first-class local adapter. */
|
|
669
|
+
declare const AGENT_HARNESSES: readonly ["claude", "codex", "cursor", "copilot", "gemini", "agents"];
|
|
670
|
+
/** A supported coding-agent harness. `agents` is the portable `AGENTS.md` adapter. */
|
|
671
|
+
type AgentHarness = (typeof AGENT_HARNESSES)[number];
|
|
672
|
+
/** A harness name accepted by setup; `all` expands to every project-local adapter. */
|
|
673
|
+
type AgentHarnessSelector = AgentHarness | "all";
|
|
674
|
+
/** Controls project-local or global installation of the bundled agent runbooks. */
|
|
332
675
|
interface SkillInstallOptions {
|
|
333
|
-
/** Project directory receiving
|
|
676
|
+
/** Project directory receiving the shared runbooks and harness adapters. Defaults to cwd. */
|
|
334
677
|
dir?: string;
|
|
335
|
-
/** Install
|
|
678
|
+
/** Install user-wide Claude and/or Codex skills instead of project adapters. */
|
|
336
679
|
global?: boolean;
|
|
337
|
-
/**
|
|
680
|
+
/** Harnesses to install. The programmatic compatibility default is `claude`; the CLI passes `all`. */
|
|
681
|
+
harnesses?: AgentHarnessSelector[];
|
|
682
|
+
/** Overwrite locally modified odla-managed files or managed instruction sections. */
|
|
338
683
|
force?: boolean;
|
|
339
684
|
/** Test/embedding override for the home directory. */
|
|
340
685
|
homeDir?: string;
|
|
686
|
+
/** Test/embedding override for `$CODEX_HOME`. */
|
|
687
|
+
codexHomeDir?: string;
|
|
341
688
|
/** Test/embedding override for the bundled skills directory. */
|
|
342
689
|
sourceDir?: string;
|
|
343
690
|
stdout?: Pick<typeof console, "log" | "error">;
|
|
344
691
|
}
|
|
692
|
+
/** One harness adapter and the directories or files through which it discovers odla. */
|
|
693
|
+
interface HarnessInstallResult {
|
|
694
|
+
harness: AgentHarness;
|
|
695
|
+
targets: string[];
|
|
696
|
+
}
|
|
697
|
+
/** Files written or left unchanged by an idempotent agent-runbook installation. */
|
|
345
698
|
interface SkillInstallResult {
|
|
699
|
+
/** Primary native skill directory, retained for compatibility with earlier callers. */
|
|
346
700
|
targetDir: string;
|
|
701
|
+
/** Files changed directly under `targetDir`, relative to that directory. */
|
|
347
702
|
written: string[];
|
|
703
|
+
/** Files already current directly under `targetDir`, relative to that directory. */
|
|
348
704
|
unchanged: string[];
|
|
705
|
+
/** Absolute paths of every changed shared-runbook or adapter file. */
|
|
706
|
+
writtenPaths: string[];
|
|
707
|
+
/** Absolute paths of every already-current shared-runbook or adapter file. */
|
|
708
|
+
unchangedPaths: string[];
|
|
709
|
+
harnesses: HarnessInstallResult[];
|
|
349
710
|
}
|
|
350
711
|
/**
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
712
|
+
* Installs one shared offline runbook tree plus thin native adapters for the
|
|
713
|
+
* selected coding-agent harnesses. Project installs use `.agents/skills/` as
|
|
714
|
+
* the canonical bundle; Claude Code, Cursor, Copilot, Gemini, and generic
|
|
715
|
+
* `AGENTS.md` adapters point to it. Global installs support the native Claude
|
|
716
|
+
* and Codex skill roots only.
|
|
717
|
+
*
|
|
718
|
+
* Installation is idempotent and planned before any write: an odla-managed
|
|
719
|
+
* file changed by the user aborts the whole operation unless `force` is set.
|
|
720
|
+
* Existing `AGENTS.md`, `GEMINI.md`, and Copilot instructions are preserved;
|
|
721
|
+
* only the bounded odla-managed section is appended or replaced.
|
|
354
722
|
*/
|
|
355
723
|
declare function installSkill(options?: SkillInstallOptions): SkillInstallResult;
|
|
356
724
|
|
|
725
|
+
/**
|
|
726
|
+
* Verify a live, already-provisioned environment end-to-end. Resolves the target
|
|
727
|
+
* env (`options.env`, else `dev`, else the first declared env) and requires it to
|
|
728
|
+
* be declared in the config. Reads the local credentials file and checks that it
|
|
729
|
+
* belongs to this app and holds a `tenantId` + `dbKey` for the env — otherwise it
|
|
730
|
+
* throws pointing the user at `odla-ai provision --write-dev-vars`.
|
|
731
|
+
*
|
|
732
|
+
* It then makes real network calls: fetches the registry public-config (and, when
|
|
733
|
+
* `ai.provider` is configured, asserts the live provider matches), fetches the
|
|
734
|
+
* live db schema with the db key and asserts every entity from the local schema
|
|
735
|
+
* is present, and runs a `count` aggregate against the first entity to confirm
|
|
736
|
+
* the db answers queries. Progress lines are printed and it ends with `ok`.
|
|
737
|
+
* Read-only — it writes nothing and throws on the first mismatch or failed
|
|
738
|
+
* request.
|
|
739
|
+
*
|
|
740
|
+
* @param options.configPath Path to the `odla.config.mjs`.
|
|
741
|
+
* @param options.env Env to check (default `dev`, else the first declared env).
|
|
742
|
+
* @param options.fetch Injectable `fetch` (for tests/embedding).
|
|
743
|
+
* @param options.stdout Optional console-like sink (defaults to `console`).
|
|
744
|
+
*/
|
|
357
745
|
declare function smoke(options: SmokeOptions): Promise<void>;
|
|
358
746
|
|
|
359
|
-
export { type LoadedProjectConfig, type LocalCredentials, type OdlaProjectConfig, type ProvisionOptions, type ProvisionPlan, type SecretsPushOptions, type SkillInstallOptions, type SkillInstallResult, type SmokeOptions, doctor, initProject, installSkill, provision, redactSecrets, runCli, secretsPush, smoke };
|
|
747
|
+
export { AGENT_HARNESSES, type AdminAiOptions, type AgentHarness, type AgentHarnessSelector, CAPABILITIES, type HarnessInstallResult, type HostedSecurityProfile, type HostedSecurityResult, type HostedSecurityTokenRequest, type LoadedProjectConfig, type LocalCredentials, type OdlaProjectConfig, type ProvisionOptions, type ProvisionPlan, type RunHostedSecurityOptions, SYSTEM_AI_PURPOSES, type ScopedPlatformTokenOptions, type SecretsPushOptions, type SkillInstallOptions, type SkillInstallResult, type SmokeOptions, type SystemAiPurpose, adminAi, doctor, getScopedPlatformToken, initProject, installSkill, printCapabilities, provision, redactSecrets, runCli, runHostedSecurity, secretsPush, smoke };
|
|
360
748
|
```
|