@fenglimg/fabric-cli 2.0.0-rc.1 → 2.0.0-rc.11

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 (32) hide show
  1. package/README.md +6 -6
  2. package/dist/{chunk-UHNP7T7W.js → chunk-5MQ52F42.js} +347 -86
  3. package/dist/chunk-6ICJICVU.js +10 -0
  4. package/dist/chunk-AW3G7ZH5.js +576 -0
  5. package/dist/chunk-HQLEHH4O.js +321 -0
  6. package/dist/{chunk-5LOYBXWD.js → chunk-OBQU6NHO.js} +2 -52
  7. package/dist/chunk-WPTA74BY.js +184 -0
  8. package/dist/chunk-WWNXR34K.js +49 -0
  9. package/dist/doctor-RILCO5OG.js +282 -0
  10. package/dist/hooks-NX32PPEN.js +13 -0
  11. package/dist/index.js +8 -5
  12. package/dist/{init-DRHUYHYA.js → init-C56PWHID.js} +225 -491
  13. package/dist/plan-context-hint-QMUPAXIB.js +98 -0
  14. package/dist/{scan-HU2EGITF.js → scan-66EKMNAY.js} +6 -2
  15. package/dist/{serve-3LXXSBFR.js → serve-NGLXHDYC.js} +8 -4
  16. package/dist/uninstall-DBAR2JBS.js +1082 -0
  17. package/package.json +3 -3
  18. package/templates/bootstrap/CLAUDE.md +1 -1
  19. package/templates/bootstrap/codex-AGENTS-header.md +1 -1
  20. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
  21. package/templates/hooks/configs/README.md +73 -0
  22. package/templates/hooks/configs/claude-code.json +37 -0
  23. package/templates/hooks/configs/codex-hooks.json +20 -0
  24. package/templates/hooks/configs/cursor-hooks.json +20 -0
  25. package/templates/hooks/fabric-hint.cjs +1337 -0
  26. package/templates/hooks/knowledge-hint-broad.cjs +612 -0
  27. package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
  28. package/templates/hooks/lib/session-digest-writer.cjs +172 -0
  29. package/templates/skills/fabric-archive/SKILL.md +640 -0
  30. package/templates/skills/fabric-import/SKILL.md +850 -0
  31. package/templates/skills/fabric-review/SKILL.md +717 -0
  32. package/dist/doctor-DUHWLAYD.js +0 -98
@@ -0,0 +1,172 @@
1
+ /**
2
+ * v2.0.0-rc.7 T5: session-digest writer.
3
+ *
4
+ * Persist a per-session ~5KB digest under
5
+ * `<projectRoot>/.fabric/.cache/session-digests/<session_id>.md` so the
6
+ * fabric-archive Skill (Phase 0.0) can stitch together cross-session
7
+ * context when it runs after Signal A fires.
8
+ *
9
+ * Contract (non-blocking, best-effort):
10
+ * - writeDigest({ projectRoot, session_id, user_messages, edit_paths, title })
11
+ * - returns { written: boolean, path: string | null } — never throws
12
+ * - silently no-ops on ENOENT, EPERM, malformed inputs
13
+ * - caps file at SIZE_CAP_BYTES (5120 = 5KB) by truncating user_messages
14
+ * bullets from the tail (oldest preserved)
15
+ * - atomic write: temp file + rename
16
+ *
17
+ * Digest shape (markdown):
18
+ *
19
+ * # <title or fallback>
20
+ *
21
+ * _Session: <session_id> · written: <iso>_
22
+ *
23
+ * ## User messages (top 10)
24
+ *
25
+ * - <msg 1, trimmed to MAX_MSG_CHARS>
26
+ * - <msg 2, ...>
27
+ *
28
+ * ## Edits
29
+ *
30
+ * - <path 1>
31
+ * - <path 2>
32
+ */
33
+ "use strict";
34
+
35
+ const { existsSync, mkdirSync, renameSync, writeFileSync, unlinkSync } = require("node:fs");
36
+ const { dirname, join } = require("node:path");
37
+
38
+ const FABRIC_DIR = ".fabric";
39
+ const CACHE_REL = join(FABRIC_DIR, ".cache", "session-digests");
40
+ const SIZE_CAP_BYTES = 5120; // ~5KB
41
+ const MAX_USER_MESSAGES = 10;
42
+ const MAX_MSG_CHARS = 500;
43
+ const MAX_EDIT_PATHS = 60;
44
+
45
+ function sanitizeSessionId(id) {
46
+ if (typeof id !== "string") return "";
47
+ // Allow alphanumeric, dash, underscore. Strip everything else to avoid
48
+ // path traversal / weird filename characters. Hard-cap 64 chars.
49
+ const cleaned = id.replace(/[^A-Za-z0-9_\-]/g, "_").slice(0, 64);
50
+ return cleaned;
51
+ }
52
+
53
+ function truncateString(s, max) {
54
+ if (typeof s !== "string") return "";
55
+ const t = s.replace(/\s+/g, " ").trim();
56
+ if (t.length <= max) return t;
57
+ return `${t.slice(0, max - 1)}…`;
58
+ }
59
+
60
+ function renderDigest({ session_id, title, user_messages, edit_paths }) {
61
+ const safeTitle =
62
+ typeof title === "string" && title.trim().length > 0
63
+ ? truncateString(title, 120)
64
+ : "(untitled session)";
65
+ const safeSessionId = sanitizeSessionId(session_id) || "(unknown)";
66
+ const userMsgs = Array.isArray(user_messages) ? user_messages : [];
67
+ const edits = Array.isArray(edit_paths) ? edit_paths : [];
68
+
69
+ const messageBullets = userMsgs
70
+ .slice(0, MAX_USER_MESSAGES)
71
+ .map((m) => `- ${truncateString(String(m ?? ""), MAX_MSG_CHARS)}`)
72
+ .filter((line) => line.length > 2);
73
+
74
+ const editBullets = edits
75
+ .slice(0, MAX_EDIT_PATHS)
76
+ .map((p) => `- ${truncateString(String(p ?? ""), 200)}`)
77
+ .filter((line) => line.length > 2);
78
+
79
+ const messagesSection =
80
+ messageBullets.length > 0
81
+ ? messageBullets.join("\n")
82
+ : "_(no user messages captured)_";
83
+ const editsSection =
84
+ editBullets.length > 0 ? editBullets.join("\n") : "_(no edits captured)_";
85
+
86
+ return [
87
+ `# ${safeTitle}`,
88
+ "",
89
+ `_Session: ${safeSessionId} · written: ${new Date().toISOString()}_`,
90
+ "",
91
+ "## User messages (top 10)",
92
+ "",
93
+ messagesSection,
94
+ "",
95
+ "## Edits",
96
+ "",
97
+ editsSection,
98
+ "",
99
+ ].join("\n");
100
+ }
101
+
102
+ /**
103
+ * Soft-cap the rendered digest to SIZE_CAP_BYTES. If the rendered text exceeds
104
+ * the cap we drop user message bullets from the tail (keeping the earliest /
105
+ * oldest entries first since those usually frame the session goal) until the
106
+ * size fits OR we run out of trimmable content.
107
+ */
108
+ function capSize(text, original) {
109
+ if (Buffer.byteLength(text, "utf8") <= SIZE_CAP_BYTES) return text;
110
+ let userMessages = Array.isArray(original.user_messages)
111
+ ? [...original.user_messages]
112
+ : [];
113
+ let attempt = text;
114
+ while (
115
+ Buffer.byteLength(attempt, "utf8") > SIZE_CAP_BYTES &&
116
+ userMessages.length > 0
117
+ ) {
118
+ userMessages.pop();
119
+ attempt = renderDigest({ ...original, user_messages: userMessages });
120
+ }
121
+ if (Buffer.byteLength(attempt, "utf8") <= SIZE_CAP_BYTES) return attempt;
122
+ // Last resort: hard-truncate.
123
+ const buf = Buffer.from(attempt, "utf8").slice(0, SIZE_CAP_BYTES);
124
+ return buf.toString("utf8");
125
+ }
126
+
127
+ function writeDigest(opts) {
128
+ if (opts === null || typeof opts !== "object") {
129
+ return { written: false, path: null };
130
+ }
131
+ const projectRoot = typeof opts.projectRoot === "string" ? opts.projectRoot : "";
132
+ const safeSessionId = sanitizeSessionId(opts.session_id);
133
+ if (projectRoot.length === 0 || safeSessionId.length === 0) {
134
+ return { written: false, path: null };
135
+ }
136
+ const cacheDir = join(projectRoot, CACHE_REL);
137
+ const target = join(cacheDir, `${safeSessionId}.md`);
138
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
139
+
140
+ try {
141
+ const rendered = renderDigest(opts);
142
+ const capped = capSize(rendered, opts);
143
+ if (!existsSync(cacheDir)) {
144
+ mkdirSync(cacheDir, { recursive: true });
145
+ }
146
+ writeFileSync(tmp, capped, "utf8");
147
+ renameSync(tmp, target);
148
+ return { written: true, path: target };
149
+ } catch {
150
+ // Best-effort. Never let digest write failure propagate to the Stop hook.
151
+ try {
152
+ if (existsSync(tmp)) unlinkSync(tmp);
153
+ } catch {
154
+ // ignore
155
+ }
156
+ return { written: false, path: null };
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ writeDigest,
162
+ // exposed for tests
163
+ renderDigest,
164
+ capSize,
165
+ CONSTANTS: {
166
+ CACHE_REL,
167
+ SIZE_CAP_BYTES,
168
+ MAX_USER_MESSAGES,
169
+ MAX_MSG_CHARS,
170
+ MAX_EDIT_PATHS,
171
+ },
172
+ };