@contextq/cli 0.1.0 → 0.3.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.
@@ -1,62 +0,0 @@
1
- /**
2
- * skills-sync.ts — `contextq skills sync <dir>` core logic.
3
- *
4
- * T698: pulls the caller's tenant-distilled `type='procedure'` contexts
5
- * (T376) from the ContextQ API (`GET /api/agent/skills/export?format=json`,
6
- * server-side rendering lives in src/services/skill-export.service.ts) and
7
- * writes them onto disk as standard Agent Skills folders: `<dir>/<slug>/SKILL.md`.
8
- *
9
- * THE IDEMPOTENCY KEY IS `metadata.cluster_key`, NOT the folder name or the
10
- * skill's title. `runSkillDistillation()` upserts on cluster_key and
11
- * rewrites `content`/`name`/`description` IN PLACE on every scheduler tick
12
- * that adds a verified session to an existing cluster -- and because the
13
- * server-side slug includes a human-readable title prefix, a title rewrite
14
- * can change the freshly-computed slug even though the underlying cluster
15
- * (and its intended on-disk folder) is unchanged. A sync that keyed off the
16
- * freshly-computed slug/title would silently FORK a second folder on the
17
- * next run. This module instead reads each existing local folder's own
18
- * `metadata.cluster_key` back out of its `SKILL.md` frontmatter (no
19
- * separate sidecar index file to go stale) and, when a remote skill's
20
- * cluster_key matches an existing folder, REWRITES THAT FOLDER IN PLACE --
21
- * even if the server just handed back a different slug for it.
22
- *
23
- * Orphans (a local folder whose cluster_key is no longer present in the
24
- * remote payload -- the source context was deleted, archived, or its
25
- * consent was revoked) are REPORTED, never auto-deleted: the file may carry
26
- * user edits.
27
- *
28
- * Network-free and fully unit-testable: `fetchSkills` is injected rather
29
- * than hard-wired to `fetch()`, so tests exercise the on-disk merge logic
30
- * against an in-memory payload with no server needed.
31
- */
32
- export interface SkillPayload {
33
- contextId: number;
34
- /** Freshly computed by the server for THIS render -- may differ from the
35
- * folder a matching cluster_key already lives in; see module doc. */
36
- slug: string;
37
- clusterKey: string;
38
- markdown: string;
39
- }
40
- export interface SyncResult {
41
- written: string[];
42
- updated: string[];
43
- unchanged: string[];
44
- /** Folder names present on disk before this run whose cluster_key is not
45
- * in the remote payload this run. Left on disk untouched. */
46
- orphans: string[];
47
- }
48
- /**
49
- * Pull a scalar value out of the `metadata:` frontmatter block this project
50
- * renders (src/services/skill-export.service.ts / yamlString()). Format is
51
- * fixed and self-controlled (both write and read side live in this repo),
52
- * so a small anchored regex is enough -- no general YAML parser needed.
53
- * `^ <key>: "..."$` in multiline mode only matches an indented entry under
54
- * `metadata:`, never the top-level `name:`/`description:` lines.
55
- */
56
- export declare function extractFrontmatterMetadataValue(markdown: string, key: string): string | null;
57
- export declare function extractClusterKey(markdown: string): string | null;
58
- export interface SyncOptions {
59
- dir: string;
60
- fetchSkills: () => Promise<SkillPayload[]>;
61
- }
62
- export declare function syncSkills(opts: SyncOptions): Promise<SyncResult>;
@@ -1,129 +0,0 @@
1
- /**
2
- * skills-sync.ts — `contextq skills sync <dir>` core logic.
3
- *
4
- * T698: pulls the caller's tenant-distilled `type='procedure'` contexts
5
- * (T376) from the ContextQ API (`GET /api/agent/skills/export?format=json`,
6
- * server-side rendering lives in src/services/skill-export.service.ts) and
7
- * writes them onto disk as standard Agent Skills folders: `<dir>/<slug>/SKILL.md`.
8
- *
9
- * THE IDEMPOTENCY KEY IS `metadata.cluster_key`, NOT the folder name or the
10
- * skill's title. `runSkillDistillation()` upserts on cluster_key and
11
- * rewrites `content`/`name`/`description` IN PLACE on every scheduler tick
12
- * that adds a verified session to an existing cluster -- and because the
13
- * server-side slug includes a human-readable title prefix, a title rewrite
14
- * can change the freshly-computed slug even though the underlying cluster
15
- * (and its intended on-disk folder) is unchanged. A sync that keyed off the
16
- * freshly-computed slug/title would silently FORK a second folder on the
17
- * next run. This module instead reads each existing local folder's own
18
- * `metadata.cluster_key` back out of its `SKILL.md` frontmatter (no
19
- * separate sidecar index file to go stale) and, when a remote skill's
20
- * cluster_key matches an existing folder, REWRITES THAT FOLDER IN PLACE --
21
- * even if the server just handed back a different slug for it.
22
- *
23
- * Orphans (a local folder whose cluster_key is no longer present in the
24
- * remote payload -- the source context was deleted, archived, or its
25
- * consent was revoked) are REPORTED, never auto-deleted: the file may carry
26
- * user edits.
27
- *
28
- * Network-free and fully unit-testable: `fetchSkills` is injected rather
29
- * than hard-wired to `fetch()`, so tests exercise the on-disk merge logic
30
- * against an in-memory payload with no server needed.
31
- */
32
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
33
- import { join } from 'node:path';
34
- /**
35
- * Pull a scalar value out of the `metadata:` frontmatter block this project
36
- * renders (src/services/skill-export.service.ts / yamlString()). Format is
37
- * fixed and self-controlled (both write and read side live in this repo),
38
- * so a small anchored regex is enough -- no general YAML parser needed.
39
- * `^ <key>: "..."$` in multiline mode only matches an indented entry under
40
- * `metadata:`, never the top-level `name:`/`description:` lines.
41
- */
42
- export function extractFrontmatterMetadataValue(markdown, key) {
43
- const re = new RegExp(`^ {2}${key}: "((?:[^"\\\\]|\\\\.)*)"$`, 'm');
44
- const match = markdown.match(re);
45
- if (!match)
46
- return null;
47
- // Reverses yamlString()'s escaping (`\` -> `\\`, `"` -> `\"`): a backslash
48
- // followed by any one character collapses to that character.
49
- return match[1].replace(/\\(.)/g, '$1');
50
- }
51
- export function extractClusterKey(markdown) {
52
- return extractFrontmatterMetadataValue(markdown, 'cluster_key');
53
- }
54
- /**
55
- * Discover existing skill folders directly under `dir` by reading each
56
- * `SKILL.md`'s own `cluster_key` back out -- this IS the persisted index;
57
- * there is no separate sidecar file to go stale. A subdirectory with no
58
- * `SKILL.md`, or a `SKILL.md` with no parseable `cluster_key` (not one this
59
- * tool wrote, or hand-edited past recognition), is left alone entirely --
60
- * never treated as ours to manage.
61
- */
62
- function scanLocalSkills(dir) {
63
- if (!existsSync(dir))
64
- return [];
65
- const entries = [];
66
- for (const name of readdirSync(dir)) {
67
- const folderPath = join(dir, name);
68
- if (!statSync(folderPath).isDirectory())
69
- continue;
70
- const skillPath = join(folderPath, 'SKILL.md');
71
- if (!existsSync(skillPath))
72
- continue;
73
- const content = readFileSync(skillPath, 'utf-8');
74
- const clusterKey = extractClusterKey(content);
75
- if (!clusterKey)
76
- continue;
77
- entries.push({ folder: name, clusterKey });
78
- }
79
- return entries;
80
- }
81
- /** Rewrite the top-level `name: <slug>` frontmatter line to match `folder`,
82
- * so `frontmatter.name` stays equal to the actual directory name (Agent
83
- * Skills convention) even when an existing folder is being reused under a
84
- * different name than the server's freshly-computed slug. Anchored to
85
- * start-of-line so it can never touch the indented `metadata:` entries. */
86
- function withFrontmatterName(markdown, folder) {
87
- return markdown.replace(/^name: .+$/m, `name: ${folder}`);
88
- }
89
- export async function syncSkills(opts) {
90
- mkdirSync(opts.dir, { recursive: true });
91
- const local = scanLocalSkills(opts.dir);
92
- const localByClusterKey = new Map(local.map((e) => [e.clusterKey, e.folder]));
93
- const usedFolders = new Set(local.map((e) => e.folder));
94
- const remote = await opts.fetchSkills();
95
- const result = { written: [], updated: [], unchanged: [], orphans: [] };
96
- const seenClusterKeys = new Set();
97
- for (const skill of remote) {
98
- seenClusterKeys.add(skill.clusterKey);
99
- const existingFolder = localByClusterKey.get(skill.clusterKey);
100
- let folder = existingFolder;
101
- if (!folder) {
102
- // Brand-new cluster. Prefer the server-computed slug; only fall back
103
- // to a suffixed name if an UNRELATED local folder already occupies
104
- // it (astronomically unlikely given the slug's hash suffix, but
105
- // deterministic and collision-safe rather than silently colliding).
106
- folder = usedFolders.has(skill.slug) ? `${skill.slug}-c${skill.contextId}` : skill.slug;
107
- usedFolders.add(folder);
108
- localByClusterKey.set(skill.clusterKey, folder);
109
- }
110
- const markdown = folder === skill.slug ? skill.markdown : withFrontmatterName(skill.markdown, folder);
111
- const folderPath = join(opts.dir, folder);
112
- const skillPath = join(folderPath, 'SKILL.md');
113
- const previous = existsSync(skillPath) ? readFileSync(skillPath, 'utf-8') : null;
114
- if (previous === markdown) {
115
- result.unchanged.push(folder);
116
- continue;
117
- }
118
- mkdirSync(folderPath, { recursive: true });
119
- writeFileSync(skillPath, markdown, 'utf-8');
120
- result[previous === null ? 'written' : 'updated'].push(folder);
121
- }
122
- for (const entry of local) {
123
- if (!seenClusterKeys.has(entry.clusterKey)) {
124
- result.orphans.push(entry.folder);
125
- }
126
- }
127
- return result;
128
- }
129
- //# sourceMappingURL=skills-sync.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"skills-sync.js","sourceRoot":"","sources":["../src/skills-sync.ts"],"names":[],"mappings":"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"}
package/dist/slug.d.ts DELETED
@@ -1,48 +0,0 @@
1
- /**
2
- * slug.ts — shared slug validation/sanitization for --agent / --workspace.
3
- *
4
- * T656: the installer used to accept --agent / --workspace verbatim (or fall
5
- * back to basename(root), a directory name that can legally contain shell
6
- * metacharacters) and interpolate them, unescaped, into a shell command
7
- * string written into .claude/settings.local.json's SessionStart hook. That
8
- * command runs through a shell on every future session start in the target
9
- * repo, so an unsanitized value there is an auto-executing injection.
10
- *
11
- * SLUG_PATTERN mirrors the slug shape the server itself converges on: every
12
- * `slugify()` in the API (src/services/workspace.service.ts,
13
- * context.service.ts, tenant-slug.ts) lowercases, then collapses any run of
14
- * non [a-z0-9] chars to a single '-', then trims leading/trailing '-'. That
15
- * output is always a subset of [a-z0-9][a-z0-9_-]{0,63} — this pattern adds
16
- * '_' too (never produced by slugify(), but a common human-typed separator)
17
- * and stays deliberately ASCII-lowercase-only so the exact same pattern can
18
- * gate both --agent and --workspace with one rule: CLAUDE.md documents
19
- * "Workspace slugs are always lowercase" as a product convention, and this
20
- * installer extends that same lowercase-only discipline to --agent too
21
- * rather than maintaining two different regexes for two fields that both
22
- * end up as shell-interpolated env var values. This is a deliberately
23
- * STRICTER-than-the-API client-side gate (the boot API's `agent` field is
24
- * just `z.string().min(1).max(64)` with no charset check server-side) — the
25
- * goal here is never producing a value the API would reject, not permitting
26
- * everything the API happens to tolerate.
27
- */
28
- export declare const SLUG_PATTERN: RegExp;
29
- export declare const SLUG_PATTERN_DESCRIPTION = "lowercase letters, digits, \"-\", \"_\" (must start with a letter/digit, max 64 chars)";
30
- /**
31
- * Validates an EXPLICIT (user-typed) flag value against SLUG_PATTERN.
32
- * Throws a clear, actionable error naming the flag, the offending value, and
33
- * the allowed pattern -- never silently truncates or mutates an explicit
34
- * value (the caller may choose to lowercase --workspace first, since
35
- * workspace slugs are documented as always-lowercase in this product; see
36
- * index.ts). No such courtesy applies to --agent, which has no such
37
- * documented casing convention.
38
- */
39
- export declare function validateSlug(value: string, flagName: string): string;
40
- /**
41
- * Sanitizes a FALLBACK value (basename(root) -- the user never typed this,
42
- * it came from the filesystem) into something that always satisfies
43
- * SLUG_PATTERN: lowercase, replace every disallowed run with a single '-',
44
- * trim leading/trailing separators, cap at 64 chars. An empty result (e.g.
45
- * a directory name made entirely of disallowed characters) falls back to
46
- * the fixed default 'default' rather than producing an empty/invalid slug.
47
- */
48
- export declare function sanitizeSlugFallback(raw: string): string;
package/dist/slug.js DELETED
@@ -1,62 +0,0 @@
1
- /**
2
- * slug.ts — shared slug validation/sanitization for --agent / --workspace.
3
- *
4
- * T656: the installer used to accept --agent / --workspace verbatim (or fall
5
- * back to basename(root), a directory name that can legally contain shell
6
- * metacharacters) and interpolate them, unescaped, into a shell command
7
- * string written into .claude/settings.local.json's SessionStart hook. That
8
- * command runs through a shell on every future session start in the target
9
- * repo, so an unsanitized value there is an auto-executing injection.
10
- *
11
- * SLUG_PATTERN mirrors the slug shape the server itself converges on: every
12
- * `slugify()` in the API (src/services/workspace.service.ts,
13
- * context.service.ts, tenant-slug.ts) lowercases, then collapses any run of
14
- * non [a-z0-9] chars to a single '-', then trims leading/trailing '-'. That
15
- * output is always a subset of [a-z0-9][a-z0-9_-]{0,63} — this pattern adds
16
- * '_' too (never produced by slugify(), but a common human-typed separator)
17
- * and stays deliberately ASCII-lowercase-only so the exact same pattern can
18
- * gate both --agent and --workspace with one rule: CLAUDE.md documents
19
- * "Workspace slugs are always lowercase" as a product convention, and this
20
- * installer extends that same lowercase-only discipline to --agent too
21
- * rather than maintaining two different regexes for two fields that both
22
- * end up as shell-interpolated env var values. This is a deliberately
23
- * STRICTER-than-the-API client-side gate (the boot API's `agent` field is
24
- * just `z.string().min(1).max(64)` with no charset check server-side) — the
25
- * goal here is never producing a value the API would reject, not permitting
26
- * everything the API happens to tolerate.
27
- */
28
- export const SLUG_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
29
- export const SLUG_PATTERN_DESCRIPTION = 'lowercase letters, digits, "-", "_" (must start with a letter/digit, max 64 chars)';
30
- /**
31
- * Validates an EXPLICIT (user-typed) flag value against SLUG_PATTERN.
32
- * Throws a clear, actionable error naming the flag, the offending value, and
33
- * the allowed pattern -- never silently truncates or mutates an explicit
34
- * value (the caller may choose to lowercase --workspace first, since
35
- * workspace slugs are documented as always-lowercase in this product; see
36
- * index.ts). No such courtesy applies to --agent, which has no such
37
- * documented casing convention.
38
- */
39
- export function validateSlug(value, flagName) {
40
- if (!SLUG_PATTERN.test(value)) {
41
- throw new Error(`${flagName} value "${value}" is not a valid slug -- allowed: ${SLUG_PATTERN_DESCRIPTION}`);
42
- }
43
- return value;
44
- }
45
- /**
46
- * Sanitizes a FALLBACK value (basename(root) -- the user never typed this,
47
- * it came from the filesystem) into something that always satisfies
48
- * SLUG_PATTERN: lowercase, replace every disallowed run with a single '-',
49
- * trim leading/trailing separators, cap at 64 chars. An empty result (e.g.
50
- * a directory name made entirely of disallowed characters) falls back to
51
- * the fixed default 'default' rather than producing an empty/invalid slug.
52
- */
53
- export function sanitizeSlugFallback(raw) {
54
- let out = raw
55
- .toLowerCase()
56
- .replace(/[^a-z0-9_-]+/g, '-')
57
- .replace(/^[-_]+|[-_]+$/g, '');
58
- out = out.slice(0, 64);
59
- out = out.replace(/[-_]+$/g, '');
60
- return out === '' ? 'default' : out;
61
- }
62
- //# sourceMappingURL=slug.js.map
package/dist/slug.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,4BAA4B,CAAC;AAEzD,MAAM,CAAC,MAAM,wBAAwB,GAAG,oFAAoF,CAAC;AAE7H;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,QAAgB;IAC1D,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,GAAG,QAAQ,WAAW,KAAK,qCAAqC,wBAAwB,EAAE,CAC3F,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,IAAI,GAAG,GAAG,GAAG;SACV,WAAW,EAAE;SACb,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;IAEjC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvB,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAEjC,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;AACtC,CAAC"}
@@ -1,14 +0,0 @@
1
- /**
2
- * tasks-init.ts — scaffold .contextq/tasks/TASKS.md.
3
- *
4
- * Mirrors skills/contextq/scripts/ctq-tasks-init.sh 1:1 (same manifest
5
- * header, same idempotent "leave it alone if it exists" behavior, same
6
- * narrow 3-line .gitignore patch for the churny per-machine overlay files).
7
- * Does not touch anything else in .gitignore.
8
- */
9
- export interface TasksInitResult {
10
- path: string;
11
- action: 'created' | 'skipped';
12
- detail: string;
13
- }
14
- export declare function initTasksDir(root: string): TasksInitResult;
@@ -1,51 +0,0 @@
1
- /**
2
- * tasks-init.ts — scaffold .contextq/tasks/TASKS.md.
3
- *
4
- * Mirrors skills/contextq/scripts/ctq-tasks-init.sh 1:1 (same manifest
5
- * header, same idempotent "leave it alone if it exists" behavior, same
6
- * narrow 3-line .gitignore patch for the churny per-machine overlay files).
7
- * Does not touch anything else in .gitignore.
8
- */
9
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
- import { basename, join } from 'node:path';
11
- function manifestHeader(project) {
12
- return `# Tasks: ${project}
13
- <!-- ctq-tasks manifest v1 (T442, repo-local fork of nf-tasks). Status tokens: [backlog] [todo] [doing] [done] [forbidden]. -->
14
- <!-- "blocked" is DERIVED — never a manual status. Two kinds: deps not all [done], OR a -->
15
- <!-- "blocker: <reason>" line on a [todo] (external wait); clear the line to release it. -->
16
- <!-- [backlog] = not ready yet; excluded from run planning. Promote with: flip todo <id> -->
17
- <!-- [forbidden] = too dangerous to ever run — permanent tombstone. Add 'reason: <why>'. -->
18
- <!-- Never archived. Flip back to todo only if mis-classified. -->
19
- <!-- Set with: flip forbidden <id> -->
20
- <!-- Task header format is parsed: "## T<NN> [status] <free-text title>". Keep the id+status shape. -->
21
- <!-- See docs/tech/ctq-tasks-sync.md for the field mapping this manifest syncs against. -->
22
-
23
- `;
24
- }
25
- function patchGitignore(root) {
26
- if (!existsSync(join(root, '.git')))
27
- return;
28
- const gitignorePath = join(root, '.gitignore');
29
- const patterns = ['.contextq/tasks/status.log', '.contextq/tasks/.reconcile.lock/', '.contextq/tasks/sync-map.json'];
30
- const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
31
- const existingLines = new Set(existing.split('\n').map((line) => line.trim()));
32
- const missing = patterns.filter((p) => !existingLines.has(p));
33
- if (missing.length === 0)
34
- return;
35
- const block = '\n# .contextq/tasks/ — TASKS.md + TASKS_ARCHIVE.md are tracked; the\n' +
36
- '# overlay/lock/sync-map below are machine-local and ephemeral\n' +
37
- missing.map((p) => `${p}\n`).join('');
38
- appendFileSync(gitignorePath, block);
39
- }
40
- export function initTasksDir(root) {
41
- const tasksDir = join(root, '.contextq', 'tasks');
42
- const manifest = join(tasksDir, 'TASKS.md');
43
- mkdirSync(tasksDir, { recursive: true });
44
- patchGitignore(root);
45
- if (existsSync(manifest)) {
46
- return { path: manifest, action: 'skipped', detail: 'manifest already exists — left untouched' };
47
- }
48
- writeFileSync(manifest, manifestHeader(basename(root)));
49
- return { path: manifest, action: 'created', detail: 'task manifest scaffolded' };
50
- }
51
- //# sourceMappingURL=tasks-init.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tasks-init.js","sourceRoot":"","sources":["../src/tasks-init.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAQ3C,SAAS,cAAc,CAAC,OAAe;IACrC,OAAO,YAAY,OAAO;;;;;;;;;;;CAW3B,CAAC;AACF,CAAC;AAED,SAAS,cAAc,CAAC,IAAY;IAClC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAAE,OAAO;IAE5C,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,CAAC,4BAA4B,EAAE,kCAAkC,EAAE,+BAA+B,CAAC,CAAC;IAErH,MAAM,QAAQ,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/E,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEjC,MAAM,KAAK,GACT,uEAAuE;QACvE,iEAAiE;QACjE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAExC,cAAc,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAE5C,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,cAAc,CAAC,IAAI,CAAC,CAAC;IAErB,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,0CAA0C,EAAE,CAAC;IACnG,CAAC;IAED,aAAa,CAAC,QAAQ,EAAE,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAC;AACnF,CAAC"}