cclaw-cli 0.43.0 → 0.45.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.
@@ -0,0 +1,220 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RUNTIME_ROOT } from "./constants.js";
4
+ import { withDirectoryLock } from "./fs-utils.js";
5
+ import { FLOW_STAGES } from "./types.js";
6
+ const KNOWLEDGE_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
7
+ const KNOWLEDGE_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
8
+ const KNOWLEDGE_UNIVERSALITY_SET = new Set(["project", "personal", "universal"]);
9
+ const KNOWLEDGE_MATURITY_SET = new Set(["raw", "lifted-to-rule", "lifted-to-enforcement"]);
10
+ const FLOW_STAGE_SET = new Set(FLOW_STAGES);
11
+ const KNOWLEDGE_REQUIRED_KEYS = [
12
+ "type",
13
+ "trigger",
14
+ "action",
15
+ "confidence",
16
+ "domain",
17
+ "stage",
18
+ "origin_stage",
19
+ "origin_feature",
20
+ "frequency",
21
+ "universality",
22
+ "maturity",
23
+ "created",
24
+ "first_seen_ts",
25
+ "last_seen_ts",
26
+ "project"
27
+ ];
28
+ const KNOWLEDGE_ALLOWED_KEYS = new Set(KNOWLEDGE_REQUIRED_KEYS);
29
+ function knowledgePath(projectRoot) {
30
+ return path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
31
+ }
32
+ function knowledgeLockPath(projectRoot) {
33
+ return path.join(projectRoot, RUNTIME_ROOT, "state", ".knowledge.lock");
34
+ }
35
+ function normalizeUtcIso(iso) {
36
+ return iso.replace(/\.\d{3}Z$/u, "Z");
37
+ }
38
+ function nowUtcIso() {
39
+ return normalizeUtcIso(new Date().toISOString());
40
+ }
41
+ function normalizeText(value) {
42
+ return value.trim().replace(/\s+/gu, " ").toLowerCase();
43
+ }
44
+ function dedupeKey(entry) {
45
+ return [
46
+ entry.type,
47
+ normalizeText(entry.trigger),
48
+ normalizeText(entry.action),
49
+ entry.domain === null ? "null" : normalizeText(entry.domain),
50
+ entry.stage ?? "null",
51
+ entry.origin_stage ?? "null",
52
+ entry.origin_feature === null ? "null" : normalizeText(entry.origin_feature),
53
+ entry.universality,
54
+ entry.project === null ? "null" : normalizeText(entry.project)
55
+ ].join("|");
56
+ }
57
+ function isIsoUtcTimestamp(value) {
58
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(value);
59
+ }
60
+ function isNullableString(value) {
61
+ return value === null || typeof value === "string";
62
+ }
63
+ function isNullableStage(value) {
64
+ return value === null || (typeof value === "string" && FLOW_STAGE_SET.has(value));
65
+ }
66
+ export function validateKnowledgeEntry(entry) {
67
+ const errors = [];
68
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
69
+ return { ok: false, errors: ["Knowledge entry must be a JSON object."] };
70
+ }
71
+ const obj = entry;
72
+ for (const key of Object.keys(obj)) {
73
+ if (!KNOWLEDGE_ALLOWED_KEYS.has(key)) {
74
+ errors.push(`Unknown key "${key}" in knowledge entry.`);
75
+ }
76
+ }
77
+ for (const key of KNOWLEDGE_REQUIRED_KEYS) {
78
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
79
+ errors.push(`Missing required key "${key}".`);
80
+ }
81
+ }
82
+ if (!KNOWLEDGE_TYPE_SET.has(obj.type)) {
83
+ errors.push("type must be one of: rule, pattern, lesson, compound.");
84
+ }
85
+ if (typeof obj.trigger !== "string" || obj.trigger.trim().length === 0) {
86
+ errors.push("trigger must be a non-empty string.");
87
+ }
88
+ if (typeof obj.action !== "string" || obj.action.trim().length === 0) {
89
+ errors.push("action must be a non-empty string.");
90
+ }
91
+ if (!KNOWLEDGE_CONFIDENCE_SET.has(obj.confidence)) {
92
+ errors.push("confidence must be one of: high, medium, low.");
93
+ }
94
+ if (!isNullableString(obj.domain)) {
95
+ errors.push("domain must be string or null.");
96
+ }
97
+ if (!isNullableStage(obj.stage)) {
98
+ errors.push(`stage must be one of ${FLOW_STAGES.join(", ")} or null.`);
99
+ }
100
+ if (!isNullableStage(obj.origin_stage)) {
101
+ errors.push(`origin_stage must be one of ${FLOW_STAGES.join(", ")} or null.`);
102
+ }
103
+ if (!isNullableString(obj.origin_feature)) {
104
+ errors.push("origin_feature must be string or null.");
105
+ }
106
+ if (typeof obj.frequency !== "number" ||
107
+ !Number.isInteger(obj.frequency) ||
108
+ obj.frequency < 1) {
109
+ errors.push("frequency must be an integer >= 1.");
110
+ }
111
+ if (!KNOWLEDGE_UNIVERSALITY_SET.has(obj.universality)) {
112
+ errors.push("universality must be one of: project, personal, universal.");
113
+ }
114
+ if (!KNOWLEDGE_MATURITY_SET.has(obj.maturity)) {
115
+ errors.push("maturity must be one of: raw, lifted-to-rule, lifted-to-enforcement.");
116
+ }
117
+ for (const timestampField of ["created", "first_seen_ts", "last_seen_ts"]) {
118
+ const value = obj[timestampField];
119
+ if (typeof value !== "string" || !isIsoUtcTimestamp(value)) {
120
+ errors.push(`${timestampField} must be ISO UTC (YYYY-MM-DDTHH:MM:SSZ).`);
121
+ }
122
+ }
123
+ if (!isNullableString(obj.project)) {
124
+ errors.push("project must be string or null.");
125
+ }
126
+ return { ok: errors.length === 0, errors };
127
+ }
128
+ export function materializeKnowledgeEntry(seed, defaults = {}) {
129
+ const now = normalizeUtcIso(defaults.nowIso ?? nowUtcIso());
130
+ const stage = seed.stage ?? defaults.stage ?? null;
131
+ const originStage = seed.origin_stage ?? defaults.originStage ?? stage ?? null;
132
+ return {
133
+ type: seed.type,
134
+ trigger: seed.trigger.trim(),
135
+ action: seed.action.trim(),
136
+ confidence: seed.confidence,
137
+ domain: seed.domain ?? null,
138
+ stage,
139
+ origin_stage: originStage,
140
+ origin_feature: seed.origin_feature ?? defaults.originFeature ?? null,
141
+ frequency: seed.frequency ?? 1,
142
+ universality: seed.universality ?? "project",
143
+ maturity: seed.maturity ?? "raw",
144
+ created: normalizeUtcIso(seed.created ?? now),
145
+ first_seen_ts: normalizeUtcIso(seed.first_seen_ts ?? now),
146
+ last_seen_ts: normalizeUtcIso(seed.last_seen_ts ?? now),
147
+ project: seed.project ?? defaults.project ?? null
148
+ };
149
+ }
150
+ async function readExistingKnowledgeKeys(filePath) {
151
+ const keys = new Set();
152
+ try {
153
+ const raw = await fs.readFile(filePath, "utf8");
154
+ const lines = raw.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
155
+ for (const line of lines) {
156
+ try {
157
+ const parsed = JSON.parse(line);
158
+ const validated = validateKnowledgeEntry(parsed);
159
+ if (!validated.ok)
160
+ continue;
161
+ const entry = parsed;
162
+ keys.add(dedupeKey(entry));
163
+ }
164
+ catch {
165
+ // Ignore malformed historical lines for dedupe indexing.
166
+ }
167
+ }
168
+ }
169
+ catch {
170
+ // Missing file is fine — treat as empty store.
171
+ }
172
+ return keys;
173
+ }
174
+ export async function appendKnowledge(projectRoot, seeds, defaults = {}) {
175
+ if (seeds.length === 0) {
176
+ return { appended: 0, skippedDuplicates: 0, invalid: 0, errors: [], appendedEntries: [] };
177
+ }
178
+ const filePath = knowledgePath(projectRoot);
179
+ const errors = [];
180
+ const materialized = [];
181
+ for (let i = 0; i < seeds.length; i += 1) {
182
+ const seed = seeds[i];
183
+ const entry = materializeKnowledgeEntry(seed, defaults);
184
+ const validated = validateKnowledgeEntry(entry);
185
+ if (!validated.ok) {
186
+ errors.push(`entry #${i + 1}: ${validated.errors.join(" ")}`);
187
+ continue;
188
+ }
189
+ materialized.push(entry);
190
+ }
191
+ let skippedDuplicates = 0;
192
+ const appendedEntries = [];
193
+ await withDirectoryLock(knowledgeLockPath(projectRoot), async () => {
194
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
195
+ const existingKeys = await readExistingKnowledgeKeys(filePath);
196
+ const batchKeys = new Set();
197
+ const linesToAppend = [];
198
+ for (const entry of materialized) {
199
+ const key = dedupeKey(entry);
200
+ if (existingKeys.has(key) || batchKeys.has(key)) {
201
+ skippedDuplicates += 1;
202
+ continue;
203
+ }
204
+ batchKeys.add(key);
205
+ existingKeys.add(key);
206
+ appendedEntries.push(entry);
207
+ linesToAppend.push(JSON.stringify(entry));
208
+ }
209
+ if (linesToAppend.length > 0) {
210
+ await fs.appendFile(filePath, `${linesToAppend.join("\n")}\n`, "utf8");
211
+ }
212
+ });
213
+ return {
214
+ appended: appendedEntries.length,
215
+ skippedDuplicates,
216
+ invalid: errors.length,
217
+ errors,
218
+ appendedEntries
219
+ };
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {