From 4d45e95233922c4f867dd5179e827e668ef7a1df Mon Sep 17 00:00:00 2001 From: 8syncdev Date: Thu, 22 Oct 2026 08:58:26 +0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(ccc):=20company=20book=20=E2=80=94=20`?= =?UTF-8?q?8sync=20ccc=20know=20add=20--shared`,=20read=20first=20by=20eve?= =?UTF-8?q?ry=20room?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner 2026-10-22: "cây tri thức liên tục all in 1". One `_cong-ty.md` under the knowledge dir; the SessionStart digest shows it before the project's own book; `know ls` and the HQ Tri thức tab show it as "Công ty". /remember says when to use it. Co-Authored-By: Claude Fable 5.1 --- apps/hq-ui/app/api/know/route.ts | 4 +- apps/hq-ui/components/hq/know-view.tsx | 5 +- apps/hq-ui/lib/know.ts | 7 +-- apps/hq-ui/tests/know.test.ts | 5 +- assets/claude/skills/remember/SKILL.md | 2 +- crates/cli/src/verbs/ccc/help.rs | 2 +- crates/cli/src/verbs/ccc/know.rs | 75 +++++++++++++++++++++++--- 7 files changed, 83 insertions(+), 17 deletions(-) diff --git a/apps/hq-ui/app/api/know/route.ts b/apps/hq-ui/app/api/know/route.ts index dc135612..70cea866 100644 --- a/apps/hq-ui/app/api/know/route.ts +++ b/apps/hq-ui/app/api/know/route.ts @@ -20,10 +20,10 @@ export async function GET(req: Request) { const root = liveProjects(await mergedSessions(o.db), (cwd) => projectOf(cwd)).find((x) => x.root.toLowerCase() === want)?.root; if (!root) return bad("dự án này không có phòng nào đang chạy", 409); const r = await run("8sync", ["ccc", "know", "ls", "--json", "--all"], TIMEOUT_MS, root); - if (!r.ok) return json({ own: null, others: [], dir: null, error: (r.stderr.trim() || "8sync ccc know không chạy được (8sync < 0.110.41?)").slice(0, 200) }); + if (!r.ok) return json({ shared: null, own: null, others: [], dir: null, error: (r.stderr.trim() || "8sync ccc know không chạy được (8sync < 0.110.41?)").slice(0, 200) }); try { return json(parseKnow(JSON.parse(r.stdout))); } catch { - return json({ own: null, others: [], dir: null, error: "8sync ccc know ls trả JSON không hợp lệ" }); + return json({ shared: null, own: null, others: [], dir: null, error: "8sync ccc know ls trả JSON không hợp lệ" }); } } diff --git a/apps/hq-ui/components/hq/know-view.tsx b/apps/hq-ui/components/hq/know-view.tsx index ce71ae61..9ed482a5 100644 --- a/apps/hq-ui/components/hq/know-view.tsx +++ b/apps/hq-ui/components/hq/know-view.tsx @@ -46,7 +46,7 @@ export function KnowView({ token, roots, repo, active }: { token: string; roots: ); } - const n = (data?.own?.lessons.length ?? 0) + (data?.others.reduce((s, b) => s + b.lessons.length, 0) ?? 0); + const n = (data?.shared?.lessons.length ?? 0) + (data?.own?.lessons.length ?? 0) + (data?.others.reduce((s, b) => s + b.lessons.length, 0) ?? 0); return (
@@ -78,10 +78,11 @@ export function KnowView({ token, roots, repo, active }: { token: string; roots: ) : n === 0 ? ( } title="Cây tri thức còn trống"> - Phòng ghi bài học dùng được ở mọi dự án bằng 8sync ccc know add "<bài học>" (hoặc qua /remember). Chuyện riêng của repo vẫn ở su-code/KNOWLEDGE.md. + Phòng ghi bài học của dự án bằng 8sync ccc know add "<bài học>", bài học đúng cho mọi dự án thêm --shared (hoặc qua /remember). Chuyện riêng của repo vẫn ở su-code/KNOWLEDGE.md. ) : (
+ {data.shared && data.shared.lessons.length > 0 && } {data.others.length > 0 && (
diff --git a/apps/hq-ui/lib/know.ts b/apps/hq-ui/lib/know.ts index 0863082f..bcc57814 100644 --- a/apps/hq-ui/lib/know.ts +++ b/apps/hq-ui/lib/know.ts @@ -3,7 +3,7 @@ export type Lesson = { day: string | null; room: string | null; text: string }; export type Book = { root: string; lessons: Lesson[] }; -export type Know = { own: Book | null; others: Book[]; dir: string | null }; +export type Know = { /** The company book (`know add --shared`): lessons for every project. */ shared: Book | null; own: Book | null; others: Book[]; dir: string | null }; /** `- 2026-10-22 [dev] gitleaks full history eats 4 GB` → its parts; a line without the stamp is text only. */ export function lessonParts(line: string): Lesson { @@ -22,9 +22,10 @@ function book(v: unknown): Book | null { /** The CLI's JSON (`{own, others, dir}`); junk degrades to an empty tree, never throws. */ export function parseKnow(v: unknown): Know { - if (!v || typeof v !== "object") return { own: null, others: [], dir: null }; - const o = v as { own?: unknown; others?: unknown; dir?: unknown }; + if (!v || typeof v !== "object") return { shared: null, own: null, others: [], dir: null }; + const o = v as { shared?: unknown; own?: unknown; others?: unknown; dir?: unknown }; return { + shared: book(o.shared), own: book(o.own), others: Array.isArray(o.others) ? o.others.map(book).filter((b): b is Book => b != null) : [], dir: typeof o.dir === "string" ? o.dir : null, diff --git a/apps/hq-ui/tests/know.test.ts b/apps/hq-ui/tests/know.test.ts index 8b11f951..3b05747b 100644 --- a/apps/hq-ui/tests/know.test.ts +++ b/apps/hq-ui/tests/know.test.ts @@ -14,7 +14,8 @@ describe("knowledge tree (8sync ccc know ls --json)", () => { expect(k.others).toHaveLength(1); expect(k.others[0].lessons).toHaveLength(1); expect(k.dir).toBe("/h/know"); - expect(parseKnow({ own: null, others: [] })).toEqual({ own: null, others: [], dir: null }); - expect(parseKnow("junk")).toEqual({ own: null, others: [], dir: null }); + expect(parseKnow({ own: null, others: [] })).toEqual({ shared: null, own: null, others: [], dir: null }); + expect(parseKnow("junk")).toEqual({ shared: null, own: null, others: [], dir: null }); + expect(parseKnow({ shared: { root: "Công ty", lines: ["- 2026-10-22 [lead] s"] }, own: null, others: [] }).shared?.lessons[0].text).toBe("s"); }); }); diff --git a/assets/claude/skills/remember/SKILL.md b/assets/claude/skills/remember/SKILL.md index 2960d675..0f8cc05c 100644 --- a/assets/claude/skills/remember/SKILL.md +++ b/assets/claude/skills/remember/SKILL.md @@ -12,5 +12,5 @@ Ghi: $ARGUMENTS 5. Append đúng một dòng vào cuối `su-code/KNOWLEDGE.md` (repo không có → tạo file với tiêu đề `# KNOWLEDGE`): `- : — evidence: ()` 6. Quy trình nhiều bước đã validated và sẽ dùng lại → thêm mục vào `su-code/PLAYBOOKS.md` (When/Steps/Verify/Pitfalls). -7. Bài học dự án khác trên máy này cũng dùng được (công cụ, hook, guard, cách làm chung — không phải chuyện riêng của repo) → thêm `8sync ccc know add ""`: vào cây tri thức chung của máy (mỗi repo một file, SessionStart của mọi dự án đọc được, có ghi root); không ghi secret/dữ liệu khách. +7. Bài học dự án khác trên máy này cũng dùng được (công cụ, hook, guard, cách làm chung — không phải chuyện riêng của repo) → thêm `8sync ccc know add ""` (sổ của repo này, dự án khác đọc được dưới root của nó) hoặc `--shared` khi bài học đúng cho MỌI dự án (sổ công ty, mọi phòng đọc đầu tiên); không ghi secret/dữ liệu khách. Báo lại đúng dòng đã ghi. diff --git a/crates/cli/src/verbs/ccc/help.rs b/crates/cli/src/verbs/ccc/help.rs index 777ee298..3ea5fd66 100644 --- a/crates/cli/src/verbs/ccc/help.rs +++ b/crates/cli/src/verbs/ccc/help.rs @@ -66,7 +66,7 @@ TỰ ĐỘNG (gõ trong Claude Code) 8sync ccc auto company on|off [--json] một team làm mọi dự án trong scope (cờ `company` trong auto.json, giữ qua on/off/pause): phòng hết card ở dự án mình thì claim/wait lấy card gấp nhất ở dự án khác; HQ có công tắc \"Công ty\" cạnh \"Critic trả lời ngay\" 8sync ccc auto omp on [--model ]|off [--json] đội omp (phòng `8sync . `) chạy cạnh đội Claude Code: chỉ đổi `teams` (và `omp_model` khi có --model) trong auto.json, giữ mọi thứ khác; `--model /` = model của đội (phòng mở bằng `8sync . omp- --model `; giữ khi on/off không có --model; `on --omp`/`up --omp` nhận `--omp-model `); mỗi phòng omp được nhắc đích đầu hàng ≤ 1 lần/10 phút, ≤ 6 lần/giờ; tắt khi model đội (không đặt = model mặc định của omp) là anthropic/… (subscription Claude chỉ dùng trong Claude Code) 8sync ccc auto nudge --session --cwd [--model ] [--json] (extension omp gọi, --model = model đang chạy của phòng; không có → xét model mặc định của omp) {\"text\"}: lời nhắc đích hiện tại cho phòng omp, null khi auto tắt/tạm dừng, đội omp tắt, phòng chạy anthropic/…, ngoài --here, lease hết, hàng trống hoặc vừa nhắc - 8sync ccc know add \"\" [--repo ] | ls [--all] [--json] cây tri thức chung của máy: mỗi repo một file `~/.local/state/8sync/ccc/know/.md` (dòng `- [] `); SessionStart nạp bài học của dự án này trước rồi 3 dòng cuối của từng dự án khác dưới tiêu đề root của nó (đọc để học, không lẫn); su-code/KNOWLEDGE.md vẫn là memory commit của repo; CHỈ ghi bài học chung (công cụ, hook, guard, cách làm) — chuyện riêng của khách/dự án, secret, dữ liệu khách KHÔNG vào cây chung (quy tắc tách từng client) + 8sync ccc know add \"\" [--repo | --shared] | ls [--all] [--json] cây tri thức chung của máy (`--shared` = sổ công ty, đúng cho mọi dự án, mọi phòng đọc đầu tiên): mỗi repo một file `~/.local/state/8sync/ccc/know/.md` (dòng `- [] `); SessionStart nạp bài học của dự án này trước rồi 3 dòng cuối của từng dự án khác dưới tiêu đề root của nó (đọc để học, không lẫn); su-code/KNOWLEDGE.md vẫn là memory commit của repo; CHỈ ghi bài học chung (công cụ, hook, guard, cách làm) — chuyện riêng của khách/dự án, secret, dữ liệu khách KHÔNG vào cây chung (quy tắc tách từng client) 8sync ccc msg \"\" [--from ] | msg --all \"\" nhắn phòng khác của dự án này (cả phòng Claude Code `ccc . ` lẫn phòng omp `. `; trùng tên → gửi theo id hoặc ccc: / omp:): vào inbox phòng đó như dữ liệu từ phòng gửi (không phải lệnh của owner) + hiện trong chat HQ; không có phòng khớp → exit 1 8sync ccc meet [--say \"done|doing|blocked\"] [--room ] [--json] họp sprint daily của các phòng: ghi một dòng vào su-code/standup/.md (phòng nói lại trong 30 phút thì thay dòng cũ), đọc dòng của phòng khác; cần su-code/ 8sync ccc idle off | status [--json] (bật thêm gate TaskCompleted: task sửa code chưa test thì chưa cho xong) diff --git a/crates/cli/src/verbs/ccc/know.rs b/crates/cli/src/verbs/ccc/know.rs index db5f6213..ba749e74 100644 --- a/crates/cli/src/verbs/ccc/know.rs +++ b/crates/cli/src/verbs/ccc/know.rs @@ -14,7 +14,12 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Result}; use serde_json::json; -const USAGE: &str = "usage: 8sync ccc know add \"\" [--repo ] | ls [--all] [--json]"; +const USAGE: &str = "usage: 8sync ccc know add \"\" [--repo | --shared] | ls [--all] [--json]"; +/// The company book (`add --shared`): lessons true for every project, read first by every room. +pub(super) const SHARED_KEY: &str = "_cong-ty"; +pub(super) const SHARED_ROOT: &str = "Công ty"; +/// Lines of the company book in the digest. +const SHARED_LINES: usize = 5; /// Lines of the own project in the digest, and of each other project. const OWN_LINES: usize = 10; const OTHER_LINES: usize = 3; @@ -48,6 +53,33 @@ fn file(home: &Path, root: &Path) -> PathBuf { dir(home).join(format!("{}.md", key(root))) } +fn shared_file(home: &Path) -> PathBuf { + dir(home).join(format!("{SHARED_KEY}.md")) +} + +/// Append one lesson to the company book (`add --shared`), created with its `# Công ty` heading. +pub(super) fn add_shared(home: &Path, day: &str, room: Option<&str>, lesson: &str) -> Result { + let p = shared_file(home); + let l = line(day, room, lesson)?; + std::fs::create_dir_all(dir(home))?; + let mut body = std::fs::read_to_string(&p).unwrap_or_default(); + if body.is_empty() { + body = format!("# {SHARED_ROOT}\n"); + } + if !body.ends_with('\n') { + body.push('\n'); + } + body.push_str(&l); + body.push('\n'); + std::fs::write(&p, body)?; + Ok(p) +} + +/// The company book, None until the first `add --shared`. +pub(super) fn shared(home: &Path) -> Option { + std::fs::read_to_string(shared_file(home)).ok().and_then(|b| parse(&b)) +} + /// Pure: the line stored for a lesson; refused when empty. `room` = the session's registry name. pub(super) fn line(day: &str, room: Option<&str>, lesson: &str) -> Result { let l: String = lesson.split_whitespace().collect::>().join(" "); @@ -100,7 +132,7 @@ pub(super) fn books(home: &Path, root: &Path) -> (Option, Vec) { names.sort(); let others = names .into_iter() - .filter(|p| p.file_stem().is_none_or(|s| s != own_key.as_str())) + .filter(|p| p.file_stem().is_none_or(|s| s != own_key.as_str() && s != SHARED_KEY)) .filter_map(|p| std::fs::read_to_string(p).ok()) .filter_map(|b| parse(&b)) .collect(); @@ -110,9 +142,17 @@ pub(super) fn books(home: &Path, root: &Path) -> (Option, Vec) { /// Pure: the SessionStart block, at most `cap` chars, empty when nothing is known yet. Own project /// first with its last [`OWN_LINES`], then each other project under its own root heading with its /// last [`OTHER_LINES`]: a room reads its neighbours' lessons and always knows whose they are. -pub(super) fn digest_of(own: Option<&Book>, others: &[Book], cap: usize) -> String { +pub(super) fn digest_of(shared: Option<&Book>, own: Option<&Book>, others: &[Book], cap: usize) -> String { let mut out = String::new(); + // The company book first: what holds for every project on this machine. + if let Some(b) = shared.filter(|b| !b.lines.is_empty()) { + out.push_str("## Tri thức công ty (mọi dự án)\n"); + out.push_str(&b.lines[b.lines.len().saturating_sub(SHARED_LINES)..].join("\n")); + } if let Some(b) = own.filter(|b| !b.lines.is_empty()) { + if !out.is_empty() { + out.push_str("\n\n"); + } out.push_str("## Tri thức máy này — dự án này\n"); out.push_str(&b.lines[b.lines.len().saturating_sub(OWN_LINES)..].join("\n")); } @@ -137,7 +177,7 @@ pub(super) fn digest_of(own: Option<&Book>, others: &[Book], cap: usize) -> Stri /// The SessionStart block for the room at `root` (`hook session-start`). pub(super) fn digest(home: &Path, root: &Path, cap: usize) -> String { let (own, others) = books(home, root); - digest_of(own.as_ref(), &others, cap) + digest_of(shared(home).as_ref(), own.as_ref(), &others, cap) } /// `8sync ccc know …`. `room`: the caller's registry name; `day`: today as YYYY-MM-DD. @@ -145,14 +185,23 @@ pub(super) fn run(rest: &[String], home: &Path, root: &Path, room: Option<&str>, let mut it = rest.iter(); match it.next().map(String::as_str) { Some("add") => { - let (mut text, mut repo) = (Vec::new(), None); + let (mut text, mut repo, mut to_shared) = (Vec::new(), None, false); while let Some(a) = it.next() { match a.as_str() { + "--shared" => to_shared = true, "--repo" => repo = Some(PathBuf::from(it.next().filter(|v| !v.starts_with("--")).ok_or_else(|| anyhow::anyhow!("--repo cần một đường dẫn\n{USAGE}"))?)), x if x.starts_with("--") => bail!("không rõ `{x}`\n{USAGE}"), w => text.push(w.to_string()), } } + if to_shared && repo.is_some() { + bail!("--shared và --repo không đi cùng nhau\n{USAGE}"); + } + if to_shared { + let p = add_shared(home, day, room, &text.join(" "))?; + crate::ui::ok(&format!("đã ghi vào sổ công ty {}", p.display())); + return Ok(()); + } let root = match repo { Some(r) => crate::verbs::here::repo_of(&r).unwrap_or(r), None => root.to_path_buf(), @@ -167,13 +216,18 @@ pub(super) fn run(rest: &[String], home: &Path, root: &Path, room: Option<&str>, bail!("không rõ `{x}`\n{USAGE}"); } let (own, others) = books(home, root); + let company = shared(home); let take = |b: &Book| -> Vec { if all { b.lines.clone() } else { b.lines[b.lines.len().saturating_sub(LS_LINES)..].to_vec() } }; if as_json { let o = own.as_ref().map(|b| json!({ "root": b.root, "lines": take(b) })); + let c = company.as_ref().map(|b| json!({ "root": b.root, "lines": take(b) })); let rest: Vec<_> = others.iter().map(|b| json!({ "root": b.root, "lines": take(b) })).collect(); - println!("{}", json!({ "own": o, "others": rest, "dir": dir(home) })); + println!("{}", json!({ "shared": c, "own": o, "others": rest, "dir": dir(home) })); return Ok(()); } + if let Some(b) = company.as_ref().filter(|b| !b.lines.is_empty()) { + println!("# {} (mọi dự án)\n{}\n", b.root, take(b).join("\n")); + } match &own { Some(b) if !b.lines.is_empty() => println!("# {}\n{}", b.root, take(b).join("\n")), _ => println!("# {}\n(chưa có bài học: `8sync ccc know add \"\"`)", root.to_string_lossy().replace('\\', "/")), @@ -227,7 +281,16 @@ mod tests { assert!(d.contains("### C:/code/app")); let d = digest(&h, Path::new("/srv/new"), 4000); assert!(!d.contains("dự án này") && d.contains("### C:/code/app") && d.contains("### /srv/zus"), "{d}"); + // The company book: first in every project's digest, never listed among the "other projects". + add_shared(&h, "2026-10-22", Some("lead"), "gitleaks cả history ăn 4 GB: chỉ quét nhánh").unwrap(); + let d = digest(&h, a, 4000); + assert!(d.starts_with("## Tri thức công ty (mọi dự án)\n- 2026-10-22 [lead] gitleaks"), "{d}"); + assert!(d.contains("## Tri thức máy này — dự án này"), "{d}"); + let (_, others) = books(&h, a); + assert!(others.iter().all(|b| b.root != SHARED_ROOT), "shared is not an 'other project'"); + assert_eq!(shared(&h).map(|b| b.root), Some(SHARED_ROOT.to_string())); assert_eq!(digest(&home("empty"), a, 4000), ""); + assert!(shared(&home("empty2")).is_none()); // The cap cuts on a char boundary; only the last lines of a long book show. for i in 0..30 { add(&h, a, "2026-10-22", None, &format!("bài {i} ở đây")).unwrap(); From f9e300f7a8bed5f436fb18ac1f4a53f146805363 Mon Sep 17 00:00:00 2001 From: 8syncdev Date: Thu, 22 Oct 2026 09:00:02 +0700 Subject: [PATCH 2/2] =?UTF-8?q?chore(release):=20v0.110.49=20=E2=80=94=20c?= =?UTF-8?q?ompany=20book?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ Cargo.lock | 4 ++-- Cargo.toml | 2 +- assets/claude/.claude-plugin/plugin.json | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d162608..4d06faeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ versioning theo [SemVer](https://semver.org). **8sync rule:** mỗi PR cập nh ## [Unreleased] +## [0.110.49] — 2026-10-22 +### Added +- **Sổ tri thức công ty: `8sync ccc know add --shared ""`** (owner 2026-10-22: "cây tri thức liên tục all in 1, hiểu biết rộng, tận dụng kinh nghiệm lẫn nhau, không nhầm lẫn"): một sổ `Công ty` cho bài học đúng với MỌI dự án, mọi phòng đọc đầu tiên ở SessionStart (5 dòng cuối), rồi mới tới sổ dự án mình và 3 dòng cuối của từng dự án khác dưới root của nó; `know ls` in sổ công ty trước, `--json` thêm `shared`; tab Tri thức trên HQ có khối "Công ty" trên cùng. `--shared` và `--repo` không đi cùng. `/remember` ghi rõ khi nào dùng `--shared`. Test know (cả hai bên). + ## [0.110.48] — 2026-10-22 ### Changed - **Một công ty, nhiều dự án: `auto up` không mở đội thứ hai nữa** (owner 2026-10-22: "ý tôi là 1 công ty là rooms roles làm nhiều dự án… chứ ui ux vẫn mỗi folder dự án 1 công ty"). Khi `company` đang bật (`auto company on` hoặc `auto up --company`) và đội đã chạy ở repo khác, `auto up` (kể cả nút "Mở team" trên HQ) ở repo này chỉ đưa repo vào scope của auto, ghi `.claude/loop.md`, thêm card đầu nếu có, và báo "đội chung ở nhận card tại đây"; không mở phòng, không ghi team-root (revive không mở phòng ở đó). Đội chung lấy card qua `claim`/`wait` liên dự án (bản .40). Muốn đội riêng cho một repo: `auto company off` rồi `auto up`. `--add` không đổi. Test team +1 (30). diff --git a/Cargo.lock b/Cargo.lock index 08d5f40b..ec2d8361 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,7 +200,7 @@ dependencies = [ [[package]] name = "ccc-core" -version = "0.110.48" +version = "0.110.49" dependencies = [ "anyhow", "dirs", @@ -1503,7 +1503,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "su-code" -version = "0.110.48" +version = "0.110.49" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 4489c6af..7b705490 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/cli", "crates/ccc-core"] [workspace.package] -version = "0.110.48" +version = "0.110.49" edition = "2021" license = "PolyForm-Noncommercial-1.0.0" authors = ["8sync-dev <8sync.dev.1111@gmail.com>"] diff --git a/assets/claude/.claude-plugin/plugin.json b/assets/claude/.claude-plugin/plugin.json index 0d8279e4..30a05095 100644 --- a/assets/claude/.claude-plugin/plugin.json +++ b/assets/claude/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "8sync-kit", "displayName": "8sync kit", - "version": "0.110.48", + "version": "0.110.49", "description": "8 Sync kit for Claude Code: role-routed subagents (explore, task, quick-task, reviewer, designer, researcher, verifier, pm, architect, qa, critic) and the /ship /rules /sec /harden skills. Hooks, rules and the full skill set come from `8sync ccc kit`.", "author": { "name": "8 Sync", "url": "https://8syncdev.com" }, "homepage": "https://get.8syncdev.com",