@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.
- package/README.md +6 -6
- package/dist/{chunk-UHNP7T7W.js → chunk-5MQ52F42.js} +347 -86
- package/dist/chunk-6ICJICVU.js +10 -0
- package/dist/chunk-AW3G7ZH5.js +576 -0
- package/dist/chunk-HQLEHH4O.js +321 -0
- package/dist/{chunk-5LOYBXWD.js → chunk-OBQU6NHO.js} +2 -52
- package/dist/chunk-WPTA74BY.js +184 -0
- package/dist/chunk-WWNXR34K.js +49 -0
- package/dist/doctor-RILCO5OG.js +282 -0
- package/dist/hooks-NX32PPEN.js +13 -0
- package/dist/index.js +8 -5
- package/dist/{init-DRHUYHYA.js → init-C56PWHID.js} +225 -491
- package/dist/plan-context-hint-QMUPAXIB.js +98 -0
- package/dist/{scan-HU2EGITF.js → scan-66EKMNAY.js} +6 -2
- package/dist/{serve-3LXXSBFR.js → serve-NGLXHDYC.js} +8 -4
- package/dist/uninstall-DBAR2JBS.js +1082 -0
- package/package.json +3 -3
- package/templates/bootstrap/CLAUDE.md +1 -1
- package/templates/bootstrap/codex-AGENTS-header.md +1 -1
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
- package/templates/hooks/configs/README.md +73 -0
- package/templates/hooks/configs/claude-code.json +37 -0
- package/templates/hooks/configs/codex-hooks.json +20 -0
- package/templates/hooks/configs/cursor-hooks.json +20 -0
- package/templates/hooks/fabric-hint.cjs +1337 -0
- package/templates/hooks/knowledge-hint-broad.cjs +612 -0
- package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
- package/templates/hooks/lib/session-digest-writer.cjs +172 -0
- package/templates/skills/fabric-archive/SKILL.md +640 -0
- package/templates/skills/fabric-import/SKILL.md +850 -0
- package/templates/skills/fabric-review/SKILL.md +717 -0
- 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
|
+
};
|