@ozzylabs/feedradar 0.1.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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/dist/agents/_boundary.d.ts +44 -0
  4. package/dist/agents/_boundary.d.ts.map +1 -0
  5. package/dist/agents/_boundary.js +59 -0
  6. package/dist/agents/_boundary.js.map +1 -0
  7. package/dist/agents/claude-code.d.ts +32 -0
  8. package/dist/agents/claude-code.d.ts.map +1 -0
  9. package/dist/agents/claude-code.js +256 -0
  10. package/dist/agents/claude-code.js.map +1 -0
  11. package/dist/agents/codex-cli.d.ts +31 -0
  12. package/dist/agents/codex-cli.d.ts.map +1 -0
  13. package/dist/agents/codex-cli.js +303 -0
  14. package/dist/agents/codex-cli.js.map +1 -0
  15. package/dist/agents/copilot.d.ts +29 -0
  16. package/dist/agents/copilot.d.ts.map +1 -0
  17. package/dist/agents/copilot.js +282 -0
  18. package/dist/agents/copilot.js.map +1 -0
  19. package/dist/agents/gemini-cli.d.ts +30 -0
  20. package/dist/agents/gemini-cli.d.ts.map +1 -0
  21. package/dist/agents/gemini-cli.js +316 -0
  22. package/dist/agents/gemini-cli.js.map +1 -0
  23. package/dist/agents/index.d.ts +12 -0
  24. package/dist/agents/index.d.ts.map +1 -0
  25. package/dist/agents/index.js +33 -0
  26. package/dist/agents/index.js.map +1 -0
  27. package/dist/agents/types.d.ts +103 -0
  28. package/dist/agents/types.d.ts.map +1 -0
  29. package/dist/agents/types.js +2 -0
  30. package/dist/agents/types.js.map +1 -0
  31. package/dist/claude-skills/dismiss/SKILL.md +41 -0
  32. package/dist/claude-skills/research/SKILL.md +45 -0
  33. package/dist/claude-skills/review/SKILL.md +45 -0
  34. package/dist/claude-skills/update/SKILL.md +49 -0
  35. package/dist/cli/dismiss.d.ts +28 -0
  36. package/dist/cli/dismiss.d.ts.map +1 -0
  37. package/dist/cli/dismiss.js +122 -0
  38. package/dist/cli/dismiss.js.map +1 -0
  39. package/dist/cli/index.d.ts +7 -0
  40. package/dist/cli/index.d.ts.map +1 -0
  41. package/dist/cli/index.js +64 -0
  42. package/dist/cli/index.js.map +1 -0
  43. package/dist/cli/init.d.ts +148 -0
  44. package/dist/cli/init.d.ts.map +1 -0
  45. package/dist/cli/init.js +578 -0
  46. package/dist/cli/init.js.map +1 -0
  47. package/dist/cli/research.d.ts +30 -0
  48. package/dist/cli/research.d.ts.map +1 -0
  49. package/dist/cli/research.js +313 -0
  50. package/dist/cli/research.js.map +1 -0
  51. package/dist/cli/review.d.ts +34 -0
  52. package/dist/cli/review.d.ts.map +1 -0
  53. package/dist/cli/review.js +418 -0
  54. package/dist/cli/review.js.map +1 -0
  55. package/dist/cli/source.d.ts +57 -0
  56. package/dist/cli/source.d.ts.map +1 -0
  57. package/dist/cli/source.js +511 -0
  58. package/dist/cli/source.js.map +1 -0
  59. package/dist/cli/update.d.ts +43 -0
  60. package/dist/cli/update.d.ts.map +1 -0
  61. package/dist/cli/update.js +429 -0
  62. package/dist/cli/update.js.map +1 -0
  63. package/dist/cli/watch.d.ts +22 -0
  64. package/dist/cli/watch.d.ts.map +1 -0
  65. package/dist/cli/watch.js +101 -0
  66. package/dist/cli/watch.js.map +1 -0
  67. package/dist/core/config.d.ts +60 -0
  68. package/dist/core/config.d.ts.map +1 -0
  69. package/dist/core/config.js +101 -0
  70. package/dist/core/config.js.map +1 -0
  71. package/dist/core/feeds/derive-id.d.ts +43 -0
  72. package/dist/core/feeds/derive-id.d.ts.map +1 -0
  73. package/dist/core/feeds/derive-id.js +66 -0
  74. package/dist/core/feeds/derive-id.js.map +1 -0
  75. package/dist/core/feeds/github-api.d.ts +69 -0
  76. package/dist/core/feeds/github-api.d.ts.map +1 -0
  77. package/dist/core/feeds/github-api.js +161 -0
  78. package/dist/core/feeds/github-api.js.map +1 -0
  79. package/dist/core/feeds/github-releases.d.ts +3 -0
  80. package/dist/core/feeds/github-releases.d.ts.map +1 -0
  81. package/dist/core/feeds/github-releases.js +85 -0
  82. package/dist/core/feeds/github-releases.js.map +1 -0
  83. package/dist/core/feeds/html.d.ts +10 -0
  84. package/dist/core/feeds/html.d.ts.map +1 -0
  85. package/dist/core/feeds/html.js +263 -0
  86. package/dist/core/feeds/html.js.map +1 -0
  87. package/dist/core/feeds/index.d.ts +5 -0
  88. package/dist/core/feeds/index.d.ts.map +1 -0
  89. package/dist/core/feeds/index.js +18 -0
  90. package/dist/core/feeds/index.js.map +1 -0
  91. package/dist/core/feeds/npm-registry.d.ts +36 -0
  92. package/dist/core/feeds/npm-registry.d.ts.map +1 -0
  93. package/dist/core/feeds/npm-registry.js +200 -0
  94. package/dist/core/feeds/npm-registry.js.map +1 -0
  95. package/dist/core/feeds/rss.d.ts +12 -0
  96. package/dist/core/feeds/rss.d.ts.map +1 -0
  97. package/dist/core/feeds/rss.js +222 -0
  98. package/dist/core/feeds/rss.js.map +1 -0
  99. package/dist/core/feeds/types.d.ts +45 -0
  100. package/dist/core/feeds/types.d.ts.map +1 -0
  101. package/dist/core/feeds/types.js +2 -0
  102. package/dist/core/feeds/types.js.map +1 -0
  103. package/dist/core/filter.d.ts +25 -0
  104. package/dist/core/filter.d.ts.map +1 -0
  105. package/dist/core/filter.js +123 -0
  106. package/dist/core/filter.js.map +1 -0
  107. package/dist/core/injection-detector.d.ts +57 -0
  108. package/dist/core/injection-detector.d.ts.map +1 -0
  109. package/dist/core/injection-detector.js +109 -0
  110. package/dist/core/injection-detector.js.map +1 -0
  111. package/dist/core/items.d.ts +20 -0
  112. package/dist/core/items.d.ts.map +1 -0
  113. package/dist/core/items.js +105 -0
  114. package/dist/core/items.js.map +1 -0
  115. package/dist/core/state.d.ts +12 -0
  116. package/dist/core/state.d.ts.map +1 -0
  117. package/dist/core/state.js +42 -0
  118. package/dist/core/state.js.map +1 -0
  119. package/dist/core/templates.d.ts +21 -0
  120. package/dist/core/templates.d.ts.map +1 -0
  121. package/dist/core/templates.js +52 -0
  122. package/dist/core/templates.js.map +1 -0
  123. package/dist/core/watcher.d.ts +72 -0
  124. package/dist/core/watcher.d.ts.map +1 -0
  125. package/dist/core/watcher.js +240 -0
  126. package/dist/core/watcher.js.map +1 -0
  127. package/dist/gemini-commands/dismiss.toml +2 -0
  128. package/dist/gemini-commands/research.toml +2 -0
  129. package/dist/gemini-commands/review.toml +2 -0
  130. package/dist/gemini-commands/update.toml +2 -0
  131. package/dist/index.d.ts +3 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +8 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/schemas/config.d.ts +39 -0
  136. package/dist/schemas/config.d.ts.map +1 -0
  137. package/dist/schemas/config.js +23 -0
  138. package/dist/schemas/config.js.map +1 -0
  139. package/dist/schemas/index.d.ts +6 -0
  140. package/dist/schemas/index.d.ts.map +1 -0
  141. package/dist/schemas/index.js +6 -0
  142. package/dist/schemas/index.js.map +1 -0
  143. package/dist/schemas/item.d.ts +38 -0
  144. package/dist/schemas/item.d.ts.map +1 -0
  145. package/dist/schemas/item.js +34 -0
  146. package/dist/schemas/item.js.map +1 -0
  147. package/dist/schemas/research.d.ts +82 -0
  148. package/dist/schemas/research.d.ts.map +1 -0
  149. package/dist/schemas/research.js +45 -0
  150. package/dist/schemas/research.js.map +1 -0
  151. package/dist/schemas/source.d.ts +139 -0
  152. package/dist/schemas/source.d.ts.map +1 -0
  153. package/dist/schemas/source.js +127 -0
  154. package/dist/schemas/source.js.map +1 -0
  155. package/dist/schemas/state.d.ts +19 -0
  156. package/dist/schemas/state.d.ts.map +1 -0
  157. package/dist/schemas/state.js +12 -0
  158. package/dist/schemas/state.js.map +1 -0
  159. package/dist/skills/research/SKILL.md +156 -0
  160. package/dist/skills/review/SKILL.md +173 -0
  161. package/dist/skills/update/SKILL.md +200 -0
  162. package/dist/templates/agents/AGENTS.md +161 -0
  163. package/dist/templates/claude/CLAUDE.md +5 -0
  164. package/dist/templates/default.md +16 -0
  165. package/dist/templates/feedradar.md +165 -0
  166. package/dist/templates/routines/watch-daily.md +42 -0
  167. package/dist/templates/workflows/watch.yaml +70 -0
  168. package/package.json +73 -0
@@ -0,0 +1,105 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
5
+ import { ItemSchema } from "../schemas/index.js";
6
+ async function pathExists(p) {
7
+ try {
8
+ await access(p);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ /**
16
+ * Sanitize an item id for use as a filename.
17
+ *
18
+ * RSS GUIDs are commonly URLs containing `/` `:` `?`, which `path.join` would
19
+ * interpret as path separators (or that fail outright on Windows / NTFS). The
20
+ * yaml body still carries the original `id`, so this is purely a filename
21
+ * concern — `loadItems` discovers items by directory scan, not by name lookup.
22
+ *
23
+ * Strategy: keep alphanumerics + `.` `-` `_`, replace everything else with `_`,
24
+ * and append a short content hash whenever sanitization changed the string or
25
+ * the id exceeds 100 chars. The hash keeps two ids that sanitize to the same
26
+ * string from overwriting each other.
27
+ */
28
+ function safeFilename(itemId) {
29
+ const sanitized = itemId.replace(/[^A-Za-z0-9._-]/g, "_");
30
+ if (sanitized === itemId && sanitized.length <= 100) {
31
+ return sanitized;
32
+ }
33
+ const hash = createHash("sha256").update(itemId).digest("hex").slice(0, 8);
34
+ return `${sanitized.slice(0, 90)}-${hash}`;
35
+ }
36
+ /**
37
+ * Build the on-disk filename for an item.
38
+ *
39
+ * Each item is stored as `items/<sourceId>/<filename>.yaml`. Grouping by source
40
+ * keeps the directory listing manageable for users running dozens of sources
41
+ * and aligns with how `source remove` preserves history under
42
+ * `items/<sourceId>/` (see issue #12). Filenames are sanitized via
43
+ * `safeFilename` to tolerate URL-shaped ids that real RSS feeds emit.
44
+ */
45
+ function itemFile(itemsDir, sourceId, itemId) {
46
+ return join(itemsDir, sourceId, `${safeFilename(itemId)}.yaml`);
47
+ }
48
+ /**
49
+ * Load all items under `items/<sourceId>/` and return them parsed + validated.
50
+ *
51
+ * Malformed files surface as thrown errors with the offending filename so
52
+ * tooling can pinpoint the issue. Callers that want a fault-tolerant scan
53
+ * should wrap the call in try/catch per file.
54
+ */
55
+ export async function loadItems(itemsDir, sourceId) {
56
+ if (!(await pathExists(itemsDir)))
57
+ return [];
58
+ const out = [];
59
+ const sourceDirs = sourceId ? [sourceId] : await readdir(itemsDir);
60
+ for (const sid of sourceDirs) {
61
+ const dir = join(itemsDir, sid);
62
+ if (!(await pathExists(dir)))
63
+ continue;
64
+ let entries;
65
+ try {
66
+ entries = await readdir(dir);
67
+ }
68
+ catch {
69
+ continue;
70
+ }
71
+ for (const entry of entries) {
72
+ if (!entry.endsWith(".yaml"))
73
+ continue;
74
+ const raw = await readFile(join(dir, entry), "utf8");
75
+ const parsed = parseYaml(raw);
76
+ const result = ItemSchema.safeParse(parsed);
77
+ if (!result.success) {
78
+ throw new Error(`loadItems: schema mismatch in ${sid}/${entry}: ${result.error.issues
79
+ .map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
80
+ .join("; ")}`);
81
+ }
82
+ out.push(result.data);
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+ /**
88
+ * Persist items as YAML files under `items/<sourceId>/<itemId>.yaml`.
89
+ *
90
+ * Existing files for the same id are overwritten — callers are responsible
91
+ * for de-duplication via the `state.lastSeenIds` cursor before invoking this.
92
+ * The watcher uses this only for newly-detected items, so the overwrite path
93
+ * is effectively unreachable in normal operation; we still leave it permissive
94
+ * to keep the function idempotent under retry.
95
+ */
96
+ export async function saveItems(itemsDir, items) {
97
+ for (const item of items) {
98
+ const validated = ItemSchema.parse(item);
99
+ const dir = join(itemsDir, validated.sourceId);
100
+ await mkdir(dir, { recursive: true });
101
+ const file = itemFile(itemsDir, validated.sourceId, validated.id);
102
+ await writeFile(file, stringifyYaml(validated), "utf8");
103
+ }
104
+ }
105
+ //# sourceMappingURL=items.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"items.js","sourceRoot":"","sources":["../../src/core/items.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { SourceState } from "../schemas/index.js";
2
+ /**
3
+ * Load a single source's state from `state/<sourceId>.yaml`.
4
+ *
5
+ * Returns an empty record (no `lastFetchedAt` / `lastEtag` and an empty
6
+ * `lastSeenIds`) when the file does not exist, so callers can treat absence
7
+ * as "fresh start" without branching.
8
+ */
9
+ export declare function loadSourceState(stateDir: string, sourceId: string): Promise<SourceState>;
10
+ /** Persist a source's state, creating the directory if needed. */
11
+ export declare function saveSourceState(stateDir: string, state: SourceState): Promise<void>;
12
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/core/state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAgBvD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAU9F;AAED,kEAAkE;AAClE,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzF"}
@@ -0,0 +1,42 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
+ import { SourceStateSchema } from "../schemas/index.js";
5
+ async function pathExists(p) {
6
+ try {
7
+ await access(p);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function stateFile(stateDir, sourceId) {
15
+ return join(stateDir, `${sourceId}.yaml`);
16
+ }
17
+ /**
18
+ * Load a single source's state from `state/<sourceId>.yaml`.
19
+ *
20
+ * Returns an empty record (no `lastFetchedAt` / `lastEtag` and an empty
21
+ * `lastSeenIds`) when the file does not exist, so callers can treat absence
22
+ * as "fresh start" without branching.
23
+ */
24
+ export async function loadSourceState(stateDir, sourceId) {
25
+ const file = stateFile(stateDir, sourceId);
26
+ if (!(await pathExists(file))) {
27
+ return { sourceId, lastSeenIds: [] };
28
+ }
29
+ const raw = await readFile(file, "utf8");
30
+ const parsed = parseYaml(raw);
31
+ // Fill in sourceId if older files don't carry it (defensive).
32
+ const candidate = { ...(parsed ?? {}), sourceId: parsed?.sourceId ?? sourceId };
33
+ return SourceStateSchema.parse(candidate);
34
+ }
35
+ /** Persist a source's state, creating the directory if needed. */
36
+ export async function saveSourceState(stateDir, state) {
37
+ const validated = SourceStateSchema.parse(state);
38
+ const file = stateFile(stateDir, validated.sourceId);
39
+ await mkdir(dirname(file), { recursive: true });
40
+ await writeFile(file, stringifyYaml(validated), "utf8");
41
+ }
42
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../src/core/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,MAAM,CAAC;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,KAAK,UAAU,UAAU,CAAC,CAAS;IACjC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB,EAAE,QAAgB;IACnD,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,QAAQ,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB,EAAE,QAAgB;IACtE,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3C,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IACvC,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAmC,CAAC;IAChE,8DAA8D;IAC9D,MAAM,SAAS,GAAG,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,IAAI,QAAQ,EAAE,CAAC;IAChF,OAAO,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;AAC5C,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB,EAAE,KAAkB;IACxE,MAAM,SAAS,GAAG,iBAAiB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IACrD,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,SAAS,CAAC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,21 @@
1
+ export interface ResearchTemplate {
2
+ id: string;
3
+ path: string;
4
+ body: string;
5
+ }
6
+ /**
7
+ * Load `templates/<id>.md` from the workspace.
8
+ *
9
+ * The CLI defaults `--template default`, so callers will typically pass
10
+ * `"default"` here. If the file is missing we surface a clear error rather
11
+ * than silently falling back to an empty body — the agent prompt is built
12
+ * from the template, and a silently-empty template would produce confusing
13
+ * agent output that's hard to diagnose.
14
+ *
15
+ * `default` is also accepted when no `templates/default.md` exists: the
16
+ * agent's research SKILL has its own built-in default structure, so callers
17
+ * may legitimately skip provisioning a template file. In that case we return
18
+ * an empty body and the SKILL falls back to its bundled structure.
19
+ */
20
+ export declare function loadTemplate(id: string, dir: string): Promise<ResearchTemplate>;
21
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAsBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiBrF"}
@@ -0,0 +1,52 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ async function pathExists(p) {
4
+ try {
5
+ await access(p);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ /**
13
+ * Validate a template id as a safe filename component.
14
+ *
15
+ * Mirrors `source` id validation: reject path separators, dot-prefixed names,
16
+ * and shell-unsafe characters so `templates/<id>.md` cannot escape the
17
+ * templates directory.
18
+ */
19
+ function isSafeTemplateId(id) {
20
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id) && !id.includes("..");
21
+ }
22
+ /**
23
+ * Load `templates/<id>.md` from the workspace.
24
+ *
25
+ * The CLI defaults `--template default`, so callers will typically pass
26
+ * `"default"` here. If the file is missing we surface a clear error rather
27
+ * than silently falling back to an empty body — the agent prompt is built
28
+ * from the template, and a silently-empty template would produce confusing
29
+ * agent output that's hard to diagnose.
30
+ *
31
+ * `default` is also accepted when no `templates/default.md` exists: the
32
+ * agent's research SKILL has its own built-in default structure, so callers
33
+ * may legitimately skip provisioning a template file. In that case we return
34
+ * an empty body and the SKILL falls back to its bundled structure.
35
+ */
36
+ export async function loadTemplate(id, dir) {
37
+ if (!isSafeTemplateId(id)) {
38
+ throw new Error(`loadTemplate: invalid template id '${id}' (must match [A-Za-z0-9][A-Za-z0-9._-]*)`);
39
+ }
40
+ const path = join(dir, `${id}.md`);
41
+ if (!(await pathExists(path))) {
42
+ if (id === "default") {
43
+ // `default` is allowed to be absent — the bundled research SKILL ships
44
+ // its own structure. Returning an empty body signals "use built-in".
45
+ return { id, path, body: "" };
46
+ }
47
+ throw new Error(`loadTemplate: template not found: ${path}`);
48
+ }
49
+ const body = await readFile(path, "utf8");
50
+ return { id, path, body };
51
+ }
52
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAQjC,KAAK,UAAU,UAAU,CAAC,CAAS;IACjC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,EAAU;IAClC,OAAO,8BAA8B,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAU,EAAE,GAAW;IACxD,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,sCAAsC,EAAE,2CAA2C,CACpF,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IACnC,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC9B,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,uEAAuE;YACvE,qEAAqE;YACrE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAChC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,qCAAqC,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,72 @@
1
+ import type { Item, Source, SourceState } from "../schemas/index.js";
2
+ import type { FeedAdapter, FetchLike } from "./feeds/index.js";
3
+ export interface WorkspacePaths {
4
+ /** Workspace root; defaults to process.cwd() at the CLI layer. */
5
+ cwd: string;
6
+ sourcesDir?: string;
7
+ itemsDir?: string;
8
+ stateDir?: string;
9
+ }
10
+ export interface WatchRunOptions extends WorkspacePaths {
11
+ /** Limit the run to a single source id. Defaults to all sources. */
12
+ sourceId?: string;
13
+ /**
14
+ * Bootstrap mode: ingest all current entries into `lastSeenIds` without
15
+ * emitting any items. Used on first install to suppress noise from existing
16
+ * backlogs (ADR-0008 §運用: 初回ノイズ抑制).
17
+ */
18
+ bootstrap?: boolean;
19
+ /** Override the adapter registry (tests). */
20
+ getAdapter?: (kind: Source["kind"]) => FeedAdapter;
21
+ /** Override the HTTP fetcher (tests). */
22
+ fetch?: FetchLike;
23
+ /** Sinks for diagnostic output; defaults to console.* . */
24
+ log?: (message: string) => void;
25
+ warn?: (message: string) => void;
26
+ error?: (message: string) => void;
27
+ }
28
+ export interface WatchRunResult {
29
+ /** Map of sourceId → detected (filter-passing, not previously seen) items. */
30
+ detected: Record<string, Item[]>;
31
+ /** Map of sourceId → the SourceState that was persisted after the run. */
32
+ states: Record<string, SourceState>;
33
+ /** Sources that errored during fetch/parse, so the CLI can exit non-zero. */
34
+ errors: Array<{
35
+ sourceId: string;
36
+ message: string;
37
+ }>;
38
+ }
39
+ /**
40
+ * Load all enabled sources from `sources/*.yaml`.
41
+ *
42
+ * Malformed files are reported through `onError` but do not abort the load —
43
+ * one broken YAML should not block the entire run, mirroring how `source list`
44
+ * behaves (see `src/cli/source.ts`).
45
+ */
46
+ export declare function loadSources(sourcesDir: string, onError: (message: string) => void): Promise<Source[]>;
47
+ /**
48
+ * Execute one full watch cycle.
49
+ *
50
+ * Per source:
51
+ * 1. Load previous state (`state/<id>.yaml`).
52
+ * 2. Invoke the feed adapter with the previous ETag.
53
+ * 3. Filter freshly-fetched items.
54
+ * 4. Subtract anything in `lastSeenIds`.
55
+ * 5. In bootstrap mode, persist *all* fetched ids to seen without emitting
56
+ * items. Otherwise, write the new items to `items/<sourceId>/` and merge
57
+ * their ids into `lastSeenIds`.
58
+ * 6. Persist `state/<sourceId>.yaml`.
59
+ *
60
+ * Step 4 + 5 ensure idempotency: re-running the watcher does not produce
61
+ * duplicate item files. ADR-0006 filter semantics are applied in step 3.
62
+ */
63
+ export declare function watchRun(options: WatchRunOptions): Promise<WatchRunResult>;
64
+ /**
65
+ * Legacy convenience: fetch every source serially without any state I/O.
66
+ *
67
+ * Kept for callers that just want the raw items (e.g. the placeholder
68
+ * `radar watch` invocation). The real CLI now goes through
69
+ * `watchRun`, which threads state and filters.
70
+ */
71
+ export declare function watch(sources: Source[]): Promise<Item[]>;
72
+ //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/core/watcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAErE,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AA+C/D,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,WAAW,CAAC;IACnD,yCAAyC;IACzC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,2DAA2D;IAC3D,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACjC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,6EAA6E;IAC7E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtD;AAWD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,MAAM,EAAE,CAAC,CA6BnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAkHhF;AAED;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAQ9D"}
@@ -0,0 +1,240 @@
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { SourceSchema } from "../schemas/index.js";
5
+ import { getFeedAdapter } from "./feeds/index.js";
6
+ import { filterItems } from "./filter.js";
7
+ import { detectInjection } from "./injection-detector.js";
8
+ import { saveItems } from "./items.js";
9
+ import { loadSourceState, saveSourceState } from "./state.js";
10
+ async function pathExists(p) {
11
+ try {
12
+ await access(p);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /**
20
+ * Run the injection pre-filter (ADR-0009 M1a — Adopt) over an item's
21
+ * untrusted text fields and return the item with `injectionFlags` populated.
22
+ *
23
+ * Coverage: `title`, `summary`, and `raw`. The `raw` payload is structured
24
+ * (varies by adapter — RSS / Atom / npm / HTML), so we `JSON.stringify` it so
25
+ * embedded strings are still scanned without forcing each adapter to know
26
+ * about the detector. Fields that are unset / empty contribute nothing.
27
+ *
28
+ * Audit-only: the flags are recorded for later inspection by `research` /
29
+ * `review` / `update` and surface in CLI logs. We do NOT mutate `status`,
30
+ * sanitize content, or drop items — that aligns with ADR-0009 M5a (Adopt /
31
+ * user retains judgment) and M5b (Reject — no auto-drop).
32
+ */
33
+ function annotateInjectionFlags(item) {
34
+ const parts = [item.title];
35
+ if (item.summary)
36
+ parts.push(item.summary);
37
+ if (item.raw !== undefined) {
38
+ try {
39
+ parts.push(JSON.stringify(item.raw));
40
+ }
41
+ catch {
42
+ // Circular / unserializable raw payload — fall back to a coarse string
43
+ // cast so we still scan something rather than silently skip.
44
+ parts.push(String(item.raw));
45
+ }
46
+ }
47
+ const haystack = parts.join("\n");
48
+ const { matched } = detectInjection(haystack);
49
+ return { ...item, injectionFlags: matched };
50
+ }
51
+ function defaultPaths(opts) {
52
+ return {
53
+ cwd: opts.cwd,
54
+ sourcesDir: opts.sourcesDir ?? join(opts.cwd, "sources"),
55
+ itemsDir: opts.itemsDir ?? join(opts.cwd, "items"),
56
+ stateDir: opts.stateDir ?? join(opts.cwd, "state"),
57
+ };
58
+ }
59
+ /**
60
+ * Load all enabled sources from `sources/*.yaml`.
61
+ *
62
+ * Malformed files are reported through `onError` but do not abort the load —
63
+ * one broken YAML should not block the entire run, mirroring how `source list`
64
+ * behaves (see `src/cli/source.ts`).
65
+ */
66
+ export async function loadSources(sourcesDir, onError) {
67
+ if (!(await pathExists(sourcesDir)))
68
+ return [];
69
+ const entries = await readdir(sourcesDir);
70
+ const yamlFiles = entries.filter((f) => f.endsWith(".yaml")).sort();
71
+ const sources = [];
72
+ for (const filename of yamlFiles) {
73
+ const file = join(sourcesDir, filename);
74
+ let raw;
75
+ try {
76
+ raw = await readFile(file, "utf8");
77
+ }
78
+ catch (e) {
79
+ onError(`failed to read ${filename}: ${e instanceof Error ? e.message : String(e)}`);
80
+ continue;
81
+ }
82
+ let parsed;
83
+ try {
84
+ parsed = parseYaml(raw);
85
+ }
86
+ catch (e) {
87
+ onError(`invalid YAML in ${filename}: ${e instanceof Error ? e.message : String(e)}`);
88
+ continue;
89
+ }
90
+ const result = SourceSchema.safeParse(parsed);
91
+ if (!result.success) {
92
+ onError(`schema mismatch in ${filename}: ${result.error.issues[0]?.message ?? "unknown"}`);
93
+ continue;
94
+ }
95
+ sources.push(result.data);
96
+ }
97
+ return sources;
98
+ }
99
+ /**
100
+ * Execute one full watch cycle.
101
+ *
102
+ * Per source:
103
+ * 1. Load previous state (`state/<id>.yaml`).
104
+ * 2. Invoke the feed adapter with the previous ETag.
105
+ * 3. Filter freshly-fetched items.
106
+ * 4. Subtract anything in `lastSeenIds`.
107
+ * 5. In bootstrap mode, persist *all* fetched ids to seen without emitting
108
+ * items. Otherwise, write the new items to `items/<sourceId>/` and merge
109
+ * their ids into `lastSeenIds`.
110
+ * 6. Persist `state/<sourceId>.yaml`.
111
+ *
112
+ * Step 4 + 5 ensure idempotency: re-running the watcher does not produce
113
+ * duplicate item files. ADR-0006 filter semantics are applied in step 3.
114
+ */
115
+ export async function watchRun(options) {
116
+ const paths = defaultPaths(options);
117
+ const log = options.log ?? ((m) => console.log(m));
118
+ const warn = options.warn ?? ((m) => console.warn(m));
119
+ const error = options.error ?? ((m) => console.error(m));
120
+ const getAdapter = options.getAdapter ?? getFeedAdapter;
121
+ const sources = await loadSources(paths.sourcesDir, (m) => warn(`watch run: ${m}`));
122
+ const filtered = options.sourceId ? sources.filter((s) => s.id === options.sourceId) : sources;
123
+ if (filtered.length === 0) {
124
+ if (options.sourceId) {
125
+ warn(`watch run: no source with id '${options.sourceId}'`);
126
+ }
127
+ else {
128
+ log("watch run: no sources defined (use `radar source add ...`)");
129
+ }
130
+ return { detected: {}, states: {}, errors: [] };
131
+ }
132
+ const result = { detected: {}, states: {}, errors: [] };
133
+ for (const source of filtered) {
134
+ const previousState = await loadSourceState(paths.stateDir, source.id);
135
+ let adapter;
136
+ try {
137
+ adapter = getAdapter(source.kind);
138
+ }
139
+ catch (e) {
140
+ const message = e instanceof Error ? e.message : String(e);
141
+ error(`watch run: '${source.id}' adapter error: ${message}`);
142
+ result.errors.push({ sourceId: source.id, message });
143
+ continue;
144
+ }
145
+ let fetched;
146
+ let nextStatePatch;
147
+ let notModified = false;
148
+ try {
149
+ const fetchResult = await adapter.fetch(source, {
150
+ fetch: options.fetch,
151
+ state: previousState,
152
+ });
153
+ fetched = fetchResult.items;
154
+ nextStatePatch = fetchResult.state;
155
+ notModified = fetchResult.notModified ?? false;
156
+ }
157
+ catch (e) {
158
+ const message = e instanceof Error ? e.message : String(e);
159
+ error(`watch run: '${source.id}' fetch failed: ${message}`);
160
+ result.errors.push({ sourceId: source.id, message });
161
+ continue;
162
+ }
163
+ const seenIds = new Set(previousState.lastSeenIds);
164
+ let detectedItems = [];
165
+ if (options.bootstrap) {
166
+ // Bootstrap: seed the state with every id we saw so the *next* run can
167
+ // diff against it. We intentionally skip filter + emit so the user does
168
+ // not get blasted with historical detections (ADR-0008 §運用 hint).
169
+ for (const item of fetched)
170
+ seenIds.add(item.id);
171
+ log(`watch run: bootstrap '${source.id}' — ${fetched.length} ids recorded, no items written`);
172
+ }
173
+ else if (notModified) {
174
+ log(`watch run: '${source.id}' unchanged (304)`);
175
+ }
176
+ else {
177
+ const passed = filterItems(fetched, source);
178
+ const fresh = passed
179
+ .filter((item) => !seenIds.has(item.id))
180
+ .map((item) => annotateInjectionFlags(item));
181
+ if (fresh.length > 0) {
182
+ await saveItems(paths.itemsDir, fresh);
183
+ for (const item of fresh)
184
+ seenIds.add(item.id);
185
+ }
186
+ // Always add every fetched id to seen — even items that failed the
187
+ // filter should not be re-evaluated next run, since the filter result
188
+ // would not change without a content change (and we already keep the
189
+ // raw payload for any item that did pass).
190
+ for (const item of fetched)
191
+ seenIds.add(item.id);
192
+ detectedItems = fresh;
193
+ // Diagnose the most common "why are 0 items new?" pitfall: a source
194
+ // with no include keywords. `src/core/filter.ts` short-circuits to
195
+ // `match nothing` in that case (firehose guard), so fetched-but-zero
196
+ // is otherwise indistinguishable from "feed unchanged". The hint
197
+ // points the user at the YAML file they need to edit to fix it.
198
+ if (fetched.length > 0 && fresh.length === 0 && source.filters.keywords.length === 0) {
199
+ warn(`watch run: '${source.id}' has no keywords configured — all ${fetched.length} fetched item(s) were filtered out. Add keywords to sources/${source.id}.yaml to start ingesting.`);
200
+ }
201
+ log(`watch run: '${source.id}' fetched ${fetched.length} items, ${fresh.length} new after filter`);
202
+ // Surface a per-source audit summary for items that tripped the
203
+ // injection pre-filter (ADR-0009 M1a). We log once per source rather
204
+ // than per item to keep the watch output readable when a feed has many
205
+ // hits; the per-item view is available via `radar research` /
206
+ // `review` / `update` logs and in `items/<id>.yaml` directly.
207
+ const flagged = fresh.filter((i) => i.injectionFlags.length > 0);
208
+ if (flagged.length > 0) {
209
+ warn(`watch run: '${source.id}' ${flagged.length} item(s) tripped the prompt-injection pre-filter (audit-only; status unchanged). See injectionFlags in items/<id>.yaml.`);
210
+ }
211
+ }
212
+ const nextState = {
213
+ sourceId: source.id,
214
+ lastFetchedAt: nextStatePatch.lastFetchedAt ?? previousState.lastFetchedAt,
215
+ lastEtag: nextStatePatch.lastEtag ?? previousState.lastEtag,
216
+ lastSeenIds: Array.from(seenIds),
217
+ };
218
+ await saveSourceState(paths.stateDir, nextState);
219
+ result.detected[source.id] = detectedItems;
220
+ result.states[source.id] = nextState;
221
+ }
222
+ return result;
223
+ }
224
+ /**
225
+ * Legacy convenience: fetch every source serially without any state I/O.
226
+ *
227
+ * Kept for callers that just want the raw items (e.g. the placeholder
228
+ * `radar watch` invocation). The real CLI now goes through
229
+ * `watchRun`, which threads state and filters.
230
+ */
231
+ export async function watch(sources) {
232
+ const results = [];
233
+ for (const source of sources) {
234
+ const adapter = getFeedAdapter(source.kind);
235
+ const { items } = await adapter.fetch(source);
236
+ results.push(...items);
237
+ }
238
+ return results;
239
+ }
240
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../src/core/watcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ prompt = "Run `radar dismiss {{args}}` to mark the specified detected item as dismissed. This command does not invoke an LLM; the CLI flips the item status directly. Surface the resulting status and any errors from stdout/stderr."
2
+ description = "Mark a detected item as dismissed via FeedRadar (no LLM)."
@@ -0,0 +1,2 @@
1
+ prompt = "Run `radar research {{args}}` to generate a research report for the specified item. Surface the resulting file path and any errors from stdout/stderr. The canonical procedure lives in `.agents/skills/research/SKILL.md` (SSoT); this command is a thin wrapper that delegates to the CLI."
2
+ description = "Generate a research report for a detected item via FeedRadar."
@@ -0,0 +1,2 @@
1
+ prompt = "Run `radar review {{args}}` to cross-review the specified research report. Surface the resulting file path and any errors from stdout/stderr. The canonical procedure lives in `.agents/skills/review/SKILL.md` (SSoT); this command is a thin wrapper that delegates to the CLI."
2
+ description = "Cross-review an existing research report via FeedRadar."
@@ -0,0 +1,2 @@
1
+ prompt = "Run `radar update {{args}}` to regenerate the specified research report as a new v+1 version. Surface the resulting file path and any errors from stdout/stderr. The canonical procedure lives in `.agents/skills/update/SKILL.md` (SSoT); this command is a thin wrapper that delegates to the CLI."
2
+ description = "Regenerate a research report as a new version via FeedRadar."
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli/index.js";
3
+ run(process.argv.slice(2)).catch((err) => {
4
+ const message = err instanceof Error ? err.message : String(err);
5
+ console.error(`radar: ${message}`);
6
+ process.exit(1);
7
+ });
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAErC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAChD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,CAAC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}