@odla-ai/cli 0.9.0 → 0.10.1

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/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @odla-ai/cli — LLM context
2
2
 
3
- > Agent-operable CLI for odla provisioning, System AI administration, commit-pinned GitHub security jobs, Worker secrets, and smoke checks.
3
+ > Agent-operable CLI for odla provisioning, calendar consent and sync lifecycle, System AI administration, Worker secrets, security jobs, and smoke checks.
4
4
 
5
5
  > ⚠️ **Early access — pre-1.0.** Agents work from bounded runbooks; humans
6
6
  > approve credentials, production changes, releases, and merges. APIs and exact
@@ -10,7 +10,7 @@
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
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
13
+ deployment links, connect read-only Google Calendar mirrors, provision o11y ingest credentials, and transfer local or
14
14
  deployed Worker secrets without printing them. It also gives platform admins a
15
15
  scoped System AI control surface and gives app owners a provider-key-free
16
16
  hosted security command.
@@ -31,13 +31,17 @@ For a project you keep working in, install it as a dev dependency so the
31
31
  shorter `npx odla-ai` form works and the version is pinned by your lockfile:
32
32
 
33
33
  ```bash
34
- npm view @odla-ai/cli@0.9.0 version
35
- npm i -D --save-exact @odla-ai/cli@0.9.0
34
+ npm view @odla-ai/cli@0.10.1 version
35
+ npm i -D --save-exact @odla-ai/cli@0.10.1
36
36
  ```
37
37
 
38
38
  ## Prerequisites
39
39
 
40
40
  - Node.js 20 or newer (`node --version`).
41
+ - An existing odla account that has signed in at least once. For any command
42
+ that may need a fresh device grant, pass `--email <account>` or set
43
+ `ODLA_USER_EMAIL`. This value identifies who may review the request; it is not
44
+ a password or session credential, and an agent must never ask for either.
41
45
  - odla apps usually deploy as Cloudflare Workers. If you have never used
42
46
  Cloudflare:
43
47
  1. Create a free account at <https://dash.cloudflare.com/sign-up>.
@@ -55,10 +59,15 @@ npm i -D --save-exact @odla-ai/cli@0.9.0
55
59
  npx odla-ai setup
56
60
  npx odla-ai init --app-id my-app --name "My App"
57
61
  npx odla-ai doctor
62
+ npx odla-ai calendar status --env dev
63
+ npx odla-ai calendar calendars --env dev
64
+ npx odla-ai calendar connect --env dev
65
+ npx odla-ai calendar resync --env dev
66
+ npx odla-ai calendar disconnect --env dev --yes
58
67
  npx odla-ai capabilities --json
59
68
  # install SDKs, write the Worker, and create wrangler.jsonc before secret push
60
69
  npx odla-ai provision --dry-run
61
- npx odla-ai provision --write-dev-vars --push-secrets
70
+ npx odla-ai provision --email owner@example.com --write-dev-vars --push-secrets
62
71
  npx odla-ai smoke --env dev
63
72
  npx odla-ai secrets push --env dev
64
73
  npx odla-ai secrets set clerk_webhook_secret --env dev --stdin
@@ -70,7 +79,7 @@ npx @odla-ai/cli security sources --env dev
70
79
  npx @odla-ai/cli security run --source <source-id> --ref main --env dev --plan-digest <digest-from-security-plan> --ack-redacted-source
71
80
  npx @odla-ai/cli security status <job-id>
72
81
  npx @odla-ai/cli security report <job-id>
73
- npx odla-ai admin ai show
82
+ npx odla-ai admin ai show --email owner@example.com
74
83
  npx odla-ai admin ai models
75
84
  npx odla-ai admin ai set security --discovery-provider anthropic --discovery-model claude-opus-4-8 --validation-provider openai --validation-model gpt-5.5 --enabled
76
85
  npx odla-ai admin ai credentials
@@ -98,21 +107,28 @@ When using `--push-secrets`, create the Worker and its Wrangler config first.
98
107
  The CLI checks that config and `wrangler whoami` before it issues or rotates a
99
108
  shown-once credential.
100
109
 
101
- 1. Gets an `odla_dev_...` token by device handshake, or reuses
102
- `ODLA_DEV_TOKEN` / `.odla/dev-token.json`.
110
+ 1. Gets an `odla_dev_...` token by email-bound device handshake, or reuses
111
+ `ODLA_DEV_TOKEN` / `.odla/dev-token.json`. A fresh handshake requires
112
+ `--email <account>` or `ODLA_USER_EMAIL`; the matching existing account must
113
+ sign in, review the exact code, and approve it. Opening the URL alone does
114
+ not claim the request.
103
115
  In an interactive terminal it opens the approval page in your browser when
104
116
  the code is displayed. Pass `--open` to force browser launch from a
105
117
  non-interactive shell, or `--no-open` to suppress it. Browser launch is
106
118
  best-effort; the printed URL and code always remain the fallback.
107
119
  2. Creates the platform app if needed.
108
- 3. Enables configured services in every configured environment.
120
+ 3. Enables configured services in every configured environment. Calendar
121
+ intent is normalized per env and fixed to Google read-only access.
109
122
  4. Mints or reuses each env's configured service credentials: an odla-db app
110
123
  key for `db` and an ingest token for `o11y`.
111
124
  5. Pushes the configured schema and rules.
112
125
  6. Configures platform AI and stores provider keys in the tenant vault when the
113
126
  configured key env var is set.
114
- 7. Writes `.odla/credentials.local.json` with mode `0600`.
115
- 8. With `--write-dev-vars`, writes the local Worker values to `.dev.vars`; with
127
+ 7. With calendar enabled, starts a separate state-bound Google consent attempt
128
+ when needed and follows it through initial sync. The human completes OAuth;
129
+ codes and tokens never enter the CLI, repository, chat, or app.
130
+ 8. Writes `.odla/credentials.local.json` with mode `0600`.
131
+ 9. With `--write-dev-vars`, writes the local Worker values to `.dev.vars`; with
116
132
  `--push-secrets`, transfers the configured service secrets to the matching
117
133
  deployed Worker environment over Wrangler stdin.
118
134
 
@@ -129,12 +145,65 @@ The CLI preflights Wrangler first and persists the replacement before pushing;
129
145
  if that final push still fails, follow the printed `secrets push --env ...`
130
146
  retry. Do not run the rotation flag again.
131
147
 
148
+ ### Device-grant security and recovery
149
+
150
+ The account email sent during a fresh handshake is a non-secret identity hint,
151
+ not proof of identity. Unknown or never-signed-in accounts never produce a
152
+ claimable Studio request, and the public start/poll shape intentionally does not
153
+ reveal whether an account exists. The signed-in matching user must explicitly
154
+ review the exact code before it becomes pending, and only one claimed pending
155
+ or approved-but-uncollected request can be active for that user.
156
+
157
+ Developer grants are tracked by id, owner, label, scopes, creation/expiry,
158
+ last use, and revocation state. The plaintext token is delivered once to the
159
+ polling client and is never available from the inventory. Every signed-in user
160
+ can inspect and revoke their own grants under **Your agent credentials** in
161
+ Studio; platform admins additionally see the global token inventory and
162
+ metadata-only attempt evidence. Future requests with a revoked token fail.
163
+ Deleting
164
+ `.odla/dev-token.json` or `.odla/admin-token.local.json` removes the local copy
165
+ but does not revoke a token already copied elsewhere, so use Studio revocation
166
+ when compromise or accidental approval is possible.
167
+
132
168
  `smoke` verifies a provisioned environment from local credentials. It fetches
133
169
  the platform public config, checks the configured AI provider, fetches the live
134
170
  odla-db schema with the tenant key, compares expected schema entities, and runs
135
171
  a count aggregate against the first entity. It fails early with a clear message
136
172
  when provisioning has not written `.odla/credentials.local.json`.
137
173
 
174
+ ### Read-only Google Calendar
175
+
176
+ Calendar is a platform-custodied connector, not a Google credential stored by
177
+ the application. Enable both `db` and `calendar`, configure calendar ids and an
178
+ optional public Appointment Schedule `bookingPageUrl` per environment, then run
179
+ normal provision. The first run has two human checkpoints: the odla device code
180
+ establishes app-owner authority, then Google grants read-only Calendar scopes.
181
+
182
+ `calendar status --json` returns the safe connection/sync projection.
183
+ `calendar calendars --json` lists ids visible after consent; update checked-in
184
+ config and re-provision. A resulting `needs_sync` state reuses the installed
185
+ provider credential and resyncs without another OAuth attempt; an existing
186
+ authorization or initial sync is awaited rather than duplicated. `calendar
187
+ resync` requests an immediate sync, while
188
+ `calendar disconnect --yes` stops watches and deletes this app/environment
189
+ connection's encrypted platform token and cursors without purging retained
190
+ mirror rows, which therefore become stale. It does not revoke the shared
191
+ user-to-Google-OAuth-project grant, because that could invalidate other odla
192
+ connections for the same user. A future explicitly global revoke command would
193
+ need cross-connection accounting and warning. Production resync requires the
194
+ CLI's explicit `--yes` checkpoint, and disconnect always requires it. None of
195
+ these commands accepts a Google code, client secret, access token, or refresh
196
+ token.
197
+
198
+ The application uses its existing server-side `ODLA_ENDPOINT`, `ODLA_TENANT`,
199
+ and `ODLA_API_KEY`; calendar adds no app Worker secret. Never expose the app key
200
+ to a browser. Provider write actions are outside this read-only slice.
201
+
202
+ These commands operate the current read/embed reconciliation slice. The CLI has
203
+ no native slot-picker or create/reschedule/cancel command, and exposes no
204
+ rolling-window/age-out, quota-backoff, retention/purge, or provider-derived
205
+ mutation-id control. Public health remains aggregate rather than per calendar.
206
+
138
207
  The bundled build/migration skills add a passive pre-ship gate with
139
208
  `@odla-ai/security`: after the exact-version availability check, run
140
209
  `npm i -D --save-exact @odla-ai/security@0.2.2`, then
@@ -342,7 +411,7 @@ export default {
342
411
  dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
343
412
  app: { id: "my-app", name: "My App" },
344
413
  envs: ["dev"], // add "prod" deliberately; provisioning it requires --yes
345
- services: ["db", "ai", "o11y"],
414
+ services: ["db", "ai", "o11y", "calendar"],
346
415
  db: {
347
416
  schema: "./src/odla/schema.mjs",
348
417
  rules: "./src/odla/rules.mjs",
@@ -353,6 +422,20 @@ export default {
353
422
  provider: process.env.ODLA_AI_PROVIDER ?? "anthropic",
354
423
  keyEnv: "ANTHROPIC_API_KEY",
355
424
  },
425
+ calendar: {
426
+ google: {
427
+ calendars: { dev: ["primary"] },
428
+ bookingPageUrl: {
429
+ dev: "https://calendar.google.com/calendar/appointments/schedules/…",
430
+ },
431
+ match: {
432
+ summaryPrefix: "Intro", // optional
433
+ organizerSelf: true,
434
+ requireAttendees: true,
435
+ },
436
+ attendeePolicy: "full", // or "hashed"
437
+ },
438
+ },
356
439
  auth: {
357
440
  // Publishable key (public). Created + pulled by the Clerk CLI (`npx clerk`);
358
441
  // See the installed odla-migrate phase 3. Inline `pk_…` or "$ENV_VAR".
@@ -375,7 +458,7 @@ If schema is present and rules are omitted, `defaultRules: "deny"` generates
375
458
  deny-all rules for every schema entity. That is the safe default for Workers
376
459
  that mediate reads and writes with an app key.
377
460
 
378
- ## API reference (generated from dist/index.d.ts, v0.9.0)
461
+ ## API reference (generated from dist/index.d.ts, v0.10.1)
379
462
 
380
463
  ```ts
381
464
  import { SecurityReport, PlatformSecurityRun } from '@odla-ai/security';
@@ -410,6 +493,8 @@ type ScopedPlatformScope = "platform:ai:policy:read" | "platform:ai:policy:write
410
493
  interface ScopedPlatformTokenOptions {
411
494
  platform: string;
412
495
  scope: ScopedPlatformScope;
496
+ /** Existing odla account email used only when a fresh handshake is required. */
497
+ email?: string;
413
498
  open?: boolean;
414
499
  fetch?: typeof fetch;
415
500
  stdout?: Pick<typeof console, "log">;
@@ -439,6 +524,8 @@ interface AdminAiOptions {
439
524
  maxCallsPerRun?: number;
440
525
  platform?: string;
441
526
  token?: string;
527
+ /** Existing odla account email used only when a fresh handshake is required. */
528
+ email?: string;
442
529
  json?: boolean;
443
530
  open?: boolean;
444
531
  fetch?: typeof fetch;
@@ -471,79 +558,84 @@ declare function adminAi(options: AdminAiOptions): Promise<void>;
471
558
  /** Stable, machine-readable division of work between the CLI, coding agent,
472
559
  * human operator, and Studio. Printed by `odla-ai capabilities --json`. */
473
560
  declare const CAPABILITIES: {
474
- 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", "store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var", "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", "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys", "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"];
475
- 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"];
476
- 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", "approve GitHub App repository access separately from redacted-snippet disclosure"];
477
- 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", "connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"];
561
+ readonly cli: readonly ["start an email-bound device request without accepting a password or user session token", "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", "store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var", "push db schema/rules and configure platform AI, auth, and deployment links", "apply read-only Google calendar mirror and public booking-page config, then drive state-bound consent, status, discovery, resync, and disconnect flows", "validate config offline and smoke-test a provisioned db environment", "run app-attributed hosted security discovery and independent validation without provider keys", "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys", "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"];
562
+ 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", "wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"];
563
+ readonly human: readonly ["provide the existing odla account email, then sign in and explicitly review/approve the exact device code", "review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow", "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", "approve GitHub App repository access separately from redacted-snippet disclosure"];
564
+ readonly studio: readonly ["view telemetry and environment state", "let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them", "review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens", "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", "connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"];
478
565
  };
479
566
  type Output = Pick<typeof console, "log">;
480
567
  /** Print the stable responsibility boundary for humans or agent tooling. */
481
568
  declare function printCapabilities(json?: boolean, out?: Output): void;
482
569
 
483
- interface DoctorOptions {
484
- configPath: string;
485
- stdout?: Pick<typeof console, "log" | "error">;
570
+ declare const CALENDAR_STATES: readonly ["not_connected", "authorizing", "needs_sync", "initial_sync", "healthy", "degraded", "disconnected", "failed"];
571
+ /** Connection lifecycle reported by the registry; mirrors the platform's calendar states. */
572
+ type CalendarConnectionState = (typeof CALENDAR_STATES)[number];
573
+ /** Owner-visible, secret-free calendar connection and sync health. */
574
+ interface CalendarStatus {
575
+ env: string;
576
+ enabled: boolean;
577
+ /** True when the platform has provider credentials for this connection. */
578
+ connected: boolean;
579
+ provider: "google" | null;
580
+ status: CalendarConnectionState;
581
+ access?: "read";
582
+ calendars: string[];
583
+ attendeePolicy?: "full" | "hashed";
584
+ /** True once a booking projection may have established the DB privacy policy. */
585
+ attendeePolicyLocked?: boolean;
586
+ bookingPageUrl?: string | null;
587
+ grantedScopes: string[];
588
+ attemptId?: string;
589
+ lastSuccessfulSyncAt?: number;
590
+ lastFullSyncAt?: number;
591
+ watchExpiresAt?: number;
592
+ bookingCount?: number;
593
+ error?: {
594
+ code?: string;
595
+ message?: string;
596
+ };
597
+ }
598
+ /** One Google calendar the connected identity may select for mirroring. */
599
+ interface AvailableGoogleCalendar {
600
+ id: string;
601
+ summary?: string;
602
+ primary?: boolean;
603
+ selected?: boolean;
604
+ accessRole?: "freeBusyReader" | "reader" | "writer" | "owner";
486
605
  }
487
- /**
488
- * Validate and summarize an odla project config entirely offline — no network
489
- * calls and no file writes. Loads `configPath`, builds the provision plan, and
490
- * resolves the schema and rules (synthesizing deny-all rules from the schema
491
- * when `db.rules` is omitted and `db.defaultRules` isn't `false`), then prints a
492
- * summary line for the app, envs, services, schema entity count, rules namespace
493
- * count, and AI provider.
494
- *
495
- * It then collects warnings for the mistakes provision can't fix silently: a
496
- * schema with no entities, a rules namespace with no matching entity, the `ai`
497
- * service enabled without an `ai.provider`, `auth.clerk` entries that reference
498
- * an unset env var (`$FOO`), an `ai.keyEnv` that isn't set (provider key upload
499
- * will be skipped), and — when `o11y` is enabled — any env whose local
500
- * credentials lack an ingest token. It also folds in the shared rule linter,
501
- * git-tracked secret-shaped files, and wrangler config warnings. Prints `ok`
502
- * when clean, otherwise the warning list. Purely advisory: never throws on
503
- * warnings and never mutates anything.
504
- *
505
- * @param options.configPath Path to the `odla.config.mjs` to inspect.
506
- * @param options.stdout Optional console-like sink (defaults to `console`).
507
- */
508
- declare function doctor(options: DoctorOptions): Promise<void>;
509
606
 
510
- interface InitOptions {
511
- rootDir?: string;
512
- configPath?: string;
513
- appId: string;
514
- name: string;
515
- envs?: string[];
516
- services?: string[];
517
- aiProvider?: string;
518
- force?: boolean;
607
+ /** Shared inputs for the calendar lifecycle commands (status, connect,
608
+ * calendars, resync, disconnect): the project config path and env, plus
609
+ * injectable token, output, browser, clock, and polling hooks so agents and
610
+ * tests can drive the flow deterministically. */
611
+ interface CalendarLifecycleOptions {
612
+ /** Path to the checked-in odla project config. */ configPath: string;
613
+ /** Configured environment; defaults to dev when present. */ env?: string;
614
+ /** Existing developer-token override. */ token?: string;
615
+ /** Existing odla account email used only for a fresh handshake. */ email?: string;
616
+ open?: boolean;
617
+ interactive?: boolean;
618
+ yes?: boolean;
619
+ json?: boolean;
620
+ fetch?: typeof fetch;
519
621
  stdout?: Pick<typeof console, "log" | "error">;
622
+ openConsentUrl?: (url: string) => Promise<void>;
623
+ wait?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
624
+ pollTimeoutMs?: number;
625
+ now?: () => number;
626
+ signal?: AbortSignal;
520
627
  }
521
- /**
522
- * Scaffold a new odla project into `rootDir` (default: cwd). Writes an
523
- * `odla.config.mjs` (default name, overridable via `configPath`) plus starter
524
- * `src/odla/schema.mjs` (a single `notes` entity) and `src/odla/rules.mjs`
525
- * (deny-all), and creates the `src/odla` and `.odla` directories. Also runs
526
- * `ensureGitignore` so the local token/credentials paths are ignored.
527
- *
528
- * Fully local and synchronous — no network calls. The config file is only
529
- * written when it doesn't already exist unless `force` is set (otherwise it
530
- * throws); the schema/rules files are written only when missing (never
531
- * overwritten, even with `force`). `appId` must match `^[a-z0-9][a-z0-9-]*$` or
532
- * it throws. Defaults: `envs` → `["dev"]`, `services` → `["db","ai"]`,
533
- * `aiProvider` → `"anthropic"` (which also picks the `keyEnv`, e.g.
534
- * `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`/`GOOGLE_API_KEY`).
535
- *
536
- * @param options.appId Slug for the app (lowercase alphanumerics and hyphens).
537
- * @param options.name Human-readable app name embedded in the config.
538
- * @param options.rootDir Project root (defaults to `process.cwd()`).
539
- * @param options.configPath Config filename relative to root (default `odla.config.mjs`).
540
- * @param options.envs Environment names (default `["dev"]`; production is an explicit opt-in).
541
- * @param options.services Enabled services (default `["db","ai"]`).
542
- * @param options.aiProvider AI provider slug (default `"anthropic"`).
543
- * @param options.force Overwrite an existing config file instead of throwing.
544
- * @param options.stdout Optional console-like sink (defaults to `console`).
545
- */
546
- declare function initProject(options: InitOptions): void;
628
+ /** Read the owner-visible Google connection and mirror health for one env. */
629
+ declare function calendarStatus(options: CalendarLifecycleOptions): Promise<CalendarStatus>;
630
+ /** List calendars visible to the connected Google identity for config authoring. */
631
+ declare function calendarCalendars(options: CalendarLifecycleOptions): Promise<AvailableGoogleCalendar[]>;
632
+ /** Apply the public booking page setting, then complete Google consent + initial sync. */
633
+ declare function calendarConnect(options: CalendarLifecycleOptions): Promise<CalendarStatus>;
634
+ /** Ask the hosted connector for an immediate incremental/full recovery sync. */
635
+ declare function calendarResync(options: CalendarLifecycleOptions): Promise<CalendarStatus>;
636
+ /** Stop watches and delete this connection's encrypted platform token/cursors
637
+ * without purging the mirror or revoking the shared Google project grant. */
638
+ declare function calendarDisconnect(options: CalendarLifecycleOptions): Promise<CalendarStatus>;
547
639
 
548
640
  interface RunResult {
549
641
  code: number;
@@ -568,6 +660,43 @@ interface ClerkAuthConfig {
568
660
  audience?: string;
569
661
  mode?: "client" | "full";
570
662
  }
663
+ /** Read-only event selection applied by the hosted Google calendar connector. */
664
+ interface GoogleCalendarMatchConfig {
665
+ /** Mirror only events whose summary begins with this value. */
666
+ summaryPrefix?: string;
667
+ /** Require the connected identity to organize the event. Defaults to true. */
668
+ organizerSelf?: boolean;
669
+ /** Require at least one attendee. Defaults to true. */
670
+ requireAttendees?: boolean;
671
+ }
672
+ /** Google Calendar intent. OAuth credentials never belong in this config. */
673
+ interface GoogleCalendarConfig {
674
+ /** One through ten provider calendar ids to mirror in each odla environment. */
675
+ calendars: Record<OdlaEnvName, string[]>;
676
+ /** Public Google Appointment Schedule URL for static embed/link mode. */
677
+ bookingPageUrl?: Record<OdlaEnvName, string | null>;
678
+ match?: GoogleCalendarMatchConfig;
679
+ /** Store normalized attendee addresses, or only SHA-256 match keys. */
680
+ attendeePolicy?: "full" | "hashed";
681
+ }
682
+ /** The `calendar` block of `odla.config.mjs` — the Google read-mirror
683
+ * settings; valid only alongside the db service. */
684
+ interface CalendarConfig {
685
+ google: GoogleCalendarConfig;
686
+ }
687
+ /** Canonical, environment-specific config sent to the calendar controller. */
688
+ interface CalendarServiceConfig extends Record<string, unknown> {
689
+ provider: "google";
690
+ access: "read";
691
+ /** Deduplicated provider ids; the hosted connector accepts at most ten. */
692
+ calendars: string[];
693
+ match: {
694
+ organizerSelf: boolean;
695
+ requireAttendees: boolean;
696
+ summaryPrefix?: string;
697
+ };
698
+ attendeePolicy: "full" | "hashed";
699
+ }
571
700
  /** Authored `odla.config.mjs` contract consumed by doctor and provision. */
572
701
  interface OdlaProjectConfig {
573
702
  platformUrl?: string;
@@ -596,6 +725,7 @@ interface OdlaProjectConfig {
596
725
  /** Vault secret name. Defaults to @odla-ai/ai's provider default. */
597
726
  secretName?: string;
598
727
  };
728
+ calendar?: CalendarConfig;
599
729
  o11y?: {
600
730
  /** This service's name in the collector. Defaults to the app id. */
601
731
  service?: string;
@@ -654,12 +784,18 @@ interface ProvisionOptions {
654
784
  writeCredentials?: boolean;
655
785
  writeDevVars?: string | boolean;
656
786
  token?: string;
787
+ /** Existing odla account email used only when a fresh device handshake is required. */
788
+ email?: string;
657
789
  /** true forces browser launch, false disables it, undefined auto-opens only for interactive terminals. */
658
790
  open?: boolean;
659
791
  /** Test/embedding override for terminal interactivity. Defaults to stdin/stdout TTY detection. */
660
792
  interactive?: boolean;
661
793
  /** Test/embedding override for opening the approval URL. Defaults to the OS browser opener. */
662
794
  openApprovalUrl?: (url: string) => Promise<void>;
795
+ /** Test/embedding override for calendar connection polling. */
796
+ calendarPollWait?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
797
+ /** Maximum calendar consent + initial-sync wait. Defaults to ten minutes. */
798
+ calendarPollTimeoutMs?: number;
663
799
  /** Explicit consent for a non-dry-run plan containing `prod` or `production`. */
664
800
  yes?: boolean;
665
801
  fetch?: typeof fetch;
@@ -683,10 +819,89 @@ interface ProvisionPlan {
683
819
  interface SmokeOptions {
684
820
  configPath: string;
685
821
  env?: string;
822
+ /** Developer-token override used only for owner-visible calendar health. */
823
+ token?: string;
824
+ /** Existing odla account email used only when a fresh device handshake is required. */
825
+ email?: string;
826
+ open?: boolean;
827
+ interactive?: boolean;
828
+ openApprovalUrl?: (url: string) => Promise<void>;
686
829
  fetch?: typeof fetch;
687
830
  stdout?: Pick<typeof console, "log" | "error">;
688
831
  }
689
832
 
833
+ /** The only Google OAuth scope the platform ever requests: read-only calendar events. */
834
+ declare const GOOGLE_CALENDAR_READ_SCOPE = "https://www.googleapis.com/auth/calendar.events.readonly";
835
+ /** Resolve the checked-in calendar block into one controller-safe env payload. */
836
+ declare function calendarServiceConfig(cfg: LoadedProjectConfig, env: string): CalendarServiceConfig;
837
+ /** Public Appointment Schedule URL to apply separately from connector config. */
838
+ declare function calendarBookingPageUrl(cfg: LoadedProjectConfig, env: string): string | null | undefined;
839
+
840
+ interface DoctorOptions {
841
+ configPath: string;
842
+ stdout?: Pick<typeof console, "log" | "error">;
843
+ }
844
+ /**
845
+ * Validate and summarize an odla project config entirely offline — no network
846
+ * calls and no file writes. Loads `configPath`, builds the provision plan, and
847
+ * resolves the schema and rules (synthesizing deny-all rules from the schema
848
+ * when `db.rules` is omitted and `db.defaultRules` isn't `false`), then prints a
849
+ * summary line for the app, envs, services, schema entity count, rules namespace
850
+ * count, and AI provider.
851
+ *
852
+ * It then collects warnings for the mistakes provision can't fix silently: a
853
+ * schema with no entities, a rules namespace with no matching entity, the `ai`
854
+ * service enabled without an `ai.provider`, `auth.clerk` entries that reference
855
+ * an unset env var (`$FOO`), an `ai.keyEnv` that isn't set (provider key upload
856
+ * will be skipped), and — when `o11y` is enabled — any env whose local
857
+ * credentials lack an ingest token. It also folds in the shared rule linter,
858
+ * git-tracked secret-shaped files, and wrangler config warnings. Prints `ok`
859
+ * when clean, otherwise the warning list. Purely advisory: never throws on
860
+ * warnings and never mutates anything.
861
+ *
862
+ * @param options.configPath Path to the `odla.config.mjs` to inspect.
863
+ * @param options.stdout Optional console-like sink (defaults to `console`).
864
+ */
865
+ declare function doctor(options: DoctorOptions): Promise<void>;
866
+
867
+ interface InitOptions {
868
+ rootDir?: string;
869
+ configPath?: string;
870
+ appId: string;
871
+ name: string;
872
+ envs?: string[];
873
+ services?: string[];
874
+ aiProvider?: string;
875
+ force?: boolean;
876
+ stdout?: Pick<typeof console, "log" | "error">;
877
+ }
878
+ /**
879
+ * Scaffold a new odla project into `rootDir` (default: cwd). Writes an
880
+ * `odla.config.mjs` (default name, overridable via `configPath`) plus starter
881
+ * `src/odla/schema.mjs` (a single `notes` entity) and `src/odla/rules.mjs`
882
+ * (deny-all), and creates the `src/odla` and `.odla` directories. Also runs
883
+ * `ensureGitignore` so the local token/credentials paths are ignored.
884
+ *
885
+ * Fully local and synchronous — no network calls. The config file is only
886
+ * written when it doesn't already exist unless `force` is set (otherwise it
887
+ * throws); the schema/rules files are written only when missing (never
888
+ * overwritten, even with `force`). `appId` must match `^[a-z0-9][a-z0-9-]*$` or
889
+ * it throws. Defaults: `envs` → `["dev"]`, `services` → `["db","ai"]`,
890
+ * `aiProvider` → `"anthropic"` (which also picks the `keyEnv`, e.g.
891
+ * `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`/`GOOGLE_API_KEY`).
892
+ *
893
+ * @param options.appId Slug for the app (lowercase alphanumerics and hyphens).
894
+ * @param options.name Human-readable app name embedded in the config.
895
+ * @param options.rootDir Project root (defaults to `process.cwd()`).
896
+ * @param options.configPath Config filename relative to root (default `odla.config.mjs`).
897
+ * @param options.envs Environment names (default `["dev"]`; production is an explicit opt-in).
898
+ * @param options.services Enabled services (default `["db","ai"]`).
899
+ * @param options.aiProvider AI provider slug (default `"anthropic"`).
900
+ * @param options.force Overwrite an existing config file instead of throwing.
901
+ * @param options.stdout Optional console-like sink (defaults to `console`).
902
+ */
903
+ declare function initProject(options: InitOptions): void;
904
+
690
905
  /**
691
906
  * Provision an app end-to-end from its config — the primary deploy path, and
692
907
  * (apart from key rotation) idempotent, so re-running is safe. Obtains a
@@ -699,14 +914,17 @@ interface SmokeOptions {
699
914
  * (mode 0600, gitignored) and, when requested, `.dev.vars`.
700
915
  *
701
916
  * The developer token comes from (in order) `options.token`, `$ODLA_DEV_TOKEN`,
702
- * the cached `.odla` token, or a fresh device handshake — which prints a user
703
- * code + approval URL and, in interactive terminals, auto-opens the browser
917
+ * the cached `.odla` token, or a fresh email-bound device handshake — which
918
+ * prints a user code for the same signed-in account to review and, in
919
+ * interactive terminals, auto-opens the browser
704
920
  * (`open: true`/`false` forces/suppresses it). Network error bodies pass through
705
921
  * `redactSecrets` before display.
706
922
  *
707
923
  * @param options.dryRun Print the resolved plan and return before any network call/write.
708
924
  * @param options.rotateKeys Rotate db keys and o11y tokens.
709
925
  * @param options.rotateO11yToken Rotate only the o11y token.
926
+ * @param options.email Existing odla account email for a fresh handshake;
927
+ * this is an identity hint, never a password or session credential.
710
928
  * @param options.pushSecrets Preflight Wrangler, then push each env's configured
711
929
  * Worker secrets immediately after credential provisioning.
712
930
  * @param options.writeDevVars `true`/a path writes `.dev.vars`; `writeCredentials: false` skips the creds file.
@@ -764,6 +982,8 @@ interface SecretsSetOptions {
764
982
  yes?: boolean;
765
983
  /** Developer token override; defaults to $ODLA_DEV_TOKEN, the cached token, or a device handshake. */
766
984
  token?: string;
985
+ /** Existing odla account email used only when a fresh handshake is required. */
986
+ email?: string;
767
987
  open?: boolean;
768
988
  interactive?: boolean;
769
989
  openApprovalUrl?: (url: string) => Promise<void>;
@@ -1046,5 +1266,5 @@ declare function installSkill(options?: SkillInstallOptions): SkillInstallResult
1046
1266
  */
1047
1267
  declare function smoke(options: SmokeOptions): Promise<void>;
1048
1268
 
1049
- export { AGENT_HARNESSES, type AdminAiOptions, type AgentHarness, type AgentHarnessSelector, CAPABILITIES, type CliDependencies, type ConnectGitHubSecuritySourceOptions, type DisconnectGitHubSecuritySourceOptions, type FollowHostedSecurityJobOptions, type GetHostedSecurityIntentOptions, type GetHostedSecurityJobOptions, type HarnessInstallResult, type HostedSecurityProfile, type HostedSecurityResult, type HostedSecurityTokenRequest, type ListGitHubSecuritySourcesOptions, type ListHostedSecurityJobsOptions, type LoadedProjectConfig, type LocalCredentials, type OdlaProjectConfig, type ProvisionOptions, type ProvisionPlan, type RunHostedSecurityOptions, SYSTEM_AI_PURPOSES, type ScopedPlatformTokenOptions, type SecretsPushOptions, type SecretsSetOptions, type SkillInstallOptions, type SkillInstallResult, type SmokeOptions, type StartHostedSecurityJobOptions, type SystemAiPurpose, adminAi, connectGitHubSecuritySource, disconnectGitHubSecuritySource, doctor, followHostedSecurityJob, getHostedSecurityIntent, getHostedSecurityJob, getHostedSecurityPlan, getHostedSecurityReport, getScopedPlatformToken, inferGitHubRepository, initProject, installSkill, isTerminalHostedSecurityStatus, listGitHubSecuritySources, listHostedSecurityJobs, printCapabilities, provision, redactSecrets, repositoryFromGitRemote, runCli, runHostedSecurity, secretsPush, secretsSet, secretsSetClerkKey, smoke, startHostedSecurityJob };
1269
+ export { AGENT_HARNESSES, type AdminAiOptions, type AgentHarness, type AgentHarnessSelector, type AvailableGoogleCalendar, CAPABILITIES, type CalendarConfig, type CalendarConnectionState, type CalendarLifecycleOptions, type CalendarServiceConfig, type CalendarStatus, type CliDependencies, type ConnectGitHubSecuritySourceOptions, type DisconnectGitHubSecuritySourceOptions, type FollowHostedSecurityJobOptions, GOOGLE_CALENDAR_READ_SCOPE, type GetHostedSecurityIntentOptions, type GetHostedSecurityJobOptions, type GoogleCalendarConfig, type GoogleCalendarMatchConfig, type HarnessInstallResult, type HostedSecurityProfile, type HostedSecurityResult, type HostedSecurityTokenRequest, type ListGitHubSecuritySourcesOptions, type ListHostedSecurityJobsOptions, type LoadedProjectConfig, type LocalCredentials, type OdlaProjectConfig, type ProvisionOptions, type ProvisionPlan, type RunHostedSecurityOptions, SYSTEM_AI_PURPOSES, type ScopedPlatformTokenOptions, type SecretsPushOptions, type SecretsSetOptions, type SkillInstallOptions, type SkillInstallResult, type SmokeOptions, type StartHostedSecurityJobOptions, type SystemAiPurpose, adminAi, calendarBookingPageUrl, calendarCalendars, calendarConnect, calendarDisconnect, calendarResync, calendarServiceConfig, calendarStatus, connectGitHubSecuritySource, disconnectGitHubSecuritySource, doctor, followHostedSecurityJob, getHostedSecurityIntent, getHostedSecurityJob, getHostedSecurityPlan, getHostedSecurityReport, getScopedPlatformToken, inferGitHubRepository, initProject, installSkill, isTerminalHostedSecurityStatus, listGitHubSecuritySources, listHostedSecurityJobs, printCapabilities, provision, redactSecrets, repositoryFromGitRemote, runCli, runHostedSecurity, secretsPush, secretsSet, secretsSetClerkKey, smoke, startHostedSecurityJob };
1050
1270
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@odla-ai/cli",
3
- "version": "0.9.0",
4
- "description": "Agent-operable CLI for odla provisioning, System AI administration, commit-pinned GitHub security jobs, Worker secrets, and smoke checks.",
3
+ "version": "0.10.1",
4
+ "description": "Agent-operable CLI for odla provisioning, calendar consent and sync lifecycle, System AI administration, Worker secrets, security jobs, and smoke checks.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://odla.ai/docs/packages/cli",
7
7
  "repository": {
@@ -60,8 +60,8 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@odla-ai/ai": "^0.3.3",
63
- "@odla-ai/apps": "^0.7.1",
64
- "@odla-ai/db": "^0.6.2",
63
+ "@odla-ai/apps": "^0.8.0",
64
+ "@odla-ai/db": "^0.6.3",
65
65
  "@odla-ai/security": "^0.2.2"
66
66
  },
67
67
  "devDependencies": {