@flood-md/cli 0.2.8 → 0.2.10

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 CHANGED
@@ -3,6 +3,35 @@
3
3
  `npx @flood-md/cli init [--dry-run]` connects an existing React/Next.js npm project.
4
4
  `npx @flood-md/cli doctor` checks package versions and agent file presence.
5
5
 
6
+ Version 0.2.10 adds read-only JSON discovery. Use the CLI version matching
7
+ flood.json; source availability does not mean the candidate is published.
8
+
9
+ ```sh
10
+ npx @flood-md/cli@0.2.10 info
11
+ npx @flood-md/cli@0.2.10 catalog --kind workflow --group composition
12
+ npx @flood-md/cli@0.2.10 read component:data-table
13
+ npx @flood-md/cli@0.2.10 read recipe:table-bulk-actions
14
+ ```
15
+
16
+ info distinguishes connected, bundle-only, version-mismatch and missing-packages
17
+ states. catalog/read refuse mismatched or incomplete connected installations.
18
+ All three commands return JSON; failures return structured JSON on stderr and a
19
+ nonzero exit code. They do not install, modify host files or fetch remote guides.
20
+ The npx runner itself may fetch the explicitly selected package.
21
+
22
+ catalog filters by kind, group and literal metadata search. Default pages contain
23
+ at most 12 entries; --limit accepts 1–50, within a 16 KiB output budget. Follow
24
+ nextCursor; a changed query or catalog invalidates an old cursor. read returns
25
+ one known ID, at most 10,000 Unicode code points by default (maximum 40,000).
26
+ Follow nextOffset using --offset to finish instructions; --links-offset pages
27
+ optional links separately. Required prerequisites are explicit in requires.
28
+
29
+ Copied guides default to project-owned content, identified by source, customized
30
+ and hashes. --source package compares canonical content without overwriting.
31
+ Missing/linked project copies fail explicitly. A modified flood/catalog.json
32
+ cannot redirect reads. See [Agent knowledge](../../docs/agent-knowledge.md)
33
+ for the complete protocol and limits.
34
+
6
35
  Requires Node 22.12+. Installs matching tokens, React and agent packages; creates
7
36
  flood.json and every owned guide listed in the matching agent manifest: DESIGN.md,
8
37
  rules.json and manifest.json under flood/, plus SKILL.md and focused references
package/bin/flood.mjs CHANGED
@@ -3,6 +3,7 @@ import { run } from "../lib/index.mjs";
3
3
  try {
4
4
  await run(process.argv.slice(2));
5
5
  } catch (error) {
6
- console.error(`Flood: ${error.message}`);
6
+ if (['info','catalog','read'].includes(process.argv[2])) console.error(JSON.stringify({ok:false,error:{code:error.code ?? 'KNOWLEDGE_ERROR',message:error.message}}));
7
+ else console.error(`Flood: ${error.message}`);
7
8
  process.exitCode = 1;
8
9
  }
package/lib/index.mjs CHANGED
@@ -2,8 +2,10 @@ import { readFile, writeFile, mkdir, lstat } from "node:fs/promises";
2
2
  import { resolve, join, parse, relative, sep } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { spawnSync } from "node:child_process";
5
+ import { createHash } from 'node:crypto';
6
+ import { createKnowledgeReader } from '@flood-md/agent';
5
7
 
6
- export const VERSION = "0.2.8";
8
+ export const VERSION = "0.2.10";
7
9
  const packages = ["@flood-md/tokens", "@flood-md/react", "@flood-md/agent"];
8
10
  const json = async (path) => JSON.parse(await readFile(path, "utf8"));
9
11
  const agentFile = (name) =>
@@ -189,8 +191,111 @@ export async function doctor(root, log = console.log) {
189
191
  );
190
192
  }
191
193
 
192
- export async function run(args) {
194
+ async function installedVersion(root, name) {
195
+ let current = root;
196
+ while (true) {
197
+ try {
198
+ const path = join(current, 'node_modules', name, 'package.json');
199
+ const info = await lstat(path);
200
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 64 * 1024) throw Error('Invalid package manifest.');
201
+ const pkg = await json(path);
202
+ if (pkg.name !== name || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(pkg.version ?? '')) throw Error('Invalid package identity.');
203
+ return pkg.version;
204
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
205
+ const parent = resolve(current, '..'); if (parent === current) return null; current = parent;
206
+ }
207
+ }
208
+
209
+ export async function knowledgeInfo(root) {
210
+ root = await projectDirectory(root);
211
+ const app = await projectManifest(root);
212
+ const configInfo = await inspectTarget(root, 'flood.json');
213
+ let config;
214
+ if (configInfo) {
215
+ if (!configInfo.isFile() || configInfo.nlink !== 1 || configInfo.size > 64 * 1024) throw Error('Invalid flood.json.');
216
+ config = await json(join(root, 'flood.json')).catch(() => { throw Error('Malformed flood.json.'); });
217
+ if (!config || typeof config !== 'object' || Array.isArray(config) || !/^\d+\.\d+\.\d+$/.test(config.version ?? '')) throw Error('Invalid flood.json version.');
218
+ }
219
+ const versions = Object.fromEntries(await Promise.all([...packages, '@flood-md/cli'].map(async name => [name, await installedVersion(root, name)])));
220
+ const knowledge = await createKnowledgeReader().info();
221
+ const present = Object.values(versions).filter(Boolean);
222
+ let guideVersion = null;
223
+ if (config) {
224
+ const stat = await inspectTarget(root, 'flood/manifest.json');
225
+ if (!stat?.isFile() || stat.nlink !== 1 || stat.size > 64 * 1024) throw Error('Missing or invalid project guide manifest.');
226
+ const installed = await json(join(root, 'flood/manifest.json')).catch(() => { throw Error('Malformed project guide manifest.'); });
227
+ if (installed?.schemaVersion !== 1 || !/^\d+\.\d+\.\d+$/.test(installed.version ?? '')) throw Error('Invalid project guide manifest version.');
228
+ guideVersion = installed.version;
229
+ }
230
+ const mismatch = knowledge.version !== VERSION || (config && (config.version !== VERSION || guideVersion !== VERSION)) || present.some(version => version !== VERSION);
231
+ const missing = config && packages.some(name => !versions[name]);
232
+ const state = mismatch ? 'version-mismatch' : missing ? 'missing-packages' : config ? 'connected' : 'bundle-only';
233
+ const dependencies = {...app.devDependencies, ...app.dependencies};
234
+ return {schemaVersion:1, version:VERSION, project:{state, framework:dependencies.next ? 'next' : dependencies.react ? 'react' : 'unknown', configuredVersion:config?.version ?? null, guideVersion, installedVersions:versions}, knowledge};
235
+ }
236
+
237
+ async function knowledgeContext(root) {
238
+ const info = await knowledgeInfo(root);
239
+ if (['version-mismatch','missing-packages'].includes(info.project.state)) {
240
+ const error = new Error('Project packages, Flood configuration and this CLI must match before reading knowledge. Run info or the matching installed CLI.');
241
+ error.code = 'VERSION_MISMATCH'; throw error;
242
+ }
243
+ return info;
244
+ }
245
+
246
+ export async function knowledgeCatalog(root, query = {}) {
247
+ const info = await knowledgeContext(root);
248
+ return {...await createKnowledgeReader().list(query), scope:info.project.state};
249
+ }
250
+
251
+ export async function knowledgeRead(root, id, {source = 'auto', offset = 0, length = 10000, linksOffset = 0} = {}) {
252
+ if (!['auto','package'].includes(source)) throw Error('Read source must be auto or package.');
253
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 1 || length > 40000) throw Error('Invalid read range.');
254
+ const info = await knowledgeContext(root);
255
+ const reader = createKnowledgeReader();
256
+ const upstream = await reader.read(id, {length:1, linksOffset});
257
+ if (source === 'package' || info.project.state !== 'connected' || !upstream.installedTarget) return {...await reader.read(id, {offset,length,linksOffset}), customized:false, scope:info.project.state};
258
+ const stat = await inspectTarget(root, upstream.installedTarget);
259
+ if (!stat?.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024) throw Error('Selected project guide is missing, linked or exceeds the read budget.');
260
+ const bytes = await readFile(join(root, upstream.installedTarget));
261
+ if (bytes.length > 1024 * 1024) throw Error('Project guide exceeds the read budget.');
262
+ const body = new TextDecoder('utf-8', {fatal:true}).decode(bytes);
263
+ if (id === 'guide:design' && !body.split(/\r?\n/,1)[0].endsWith('· ' + VERSION)) throw Error('Project design guide version differs. Compare and merge the matching guide.');
264
+ const chars = Array.from(body);
265
+ if (offset > chars.length) throw Error('Read offset is outside the project material.');
266
+ const end = Math.min(chars.length, offset + length), sha256 = createHash('sha256').update(bytes).digest('hex');
267
+ return {...upstream, source:'project', scope:info.project.state, customized:sha256 !== upstream.sha256,
268
+ packageSha256:upstream.sha256, sha256, text:chars.slice(offset,end).join(''), offset,
269
+ totalCharacters:chars.length, truncated:end < chars.length, nextOffset:end < chars.length ? end : null};
270
+ }
271
+
272
+ function knowledgeArguments(command, args) {
273
+ const options = {}, positional = [];
274
+ const flags = command === 'catalog' ? new Set(['kind','group','search','limit','cursor']) : command === 'read' ? new Set(['offset','length','source','links-offset']) : new Set();
275
+ for (let i=0; i<args.length; i++) {
276
+ const value = args[i];
277
+ if (value === '--json') { if (options.json) throw Error('Duplicate --json.'); options.json = true; continue; }
278
+ if (!value.startsWith('--')) { positional.push(value); continue; }
279
+ const key = value.slice(2);
280
+ const optionKey = key === 'links-offset' ? 'linksOffset' : key;
281
+ if (!flags.has(key) || Object.hasOwn(options,optionKey) || args[i+1] === undefined || args[i+1].startsWith('--')) throw Error('Unknown, repeated or incomplete knowledge option.');
282
+ const raw = args[++i];
283
+ if (['offset','length','limit','links-offset'].includes(key)) {
284
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) throw Error('Knowledge ranges must be integers.');
285
+ options[optionKey] = Number(raw);
286
+ } else options[key] = raw;
287
+ }
288
+ if (positional.length !== (command === 'read' ? 1 : 0)) throw Error('Read requires one material ID; info and catalog accept only options.');
289
+ delete options.json; return {id:positional[0], options};
290
+ }
291
+
292
+ export async function run(args, {root = process.cwd(), log = console.log} = {}) {
193
293
  const [command, ...flags] = args;
294
+ if (['info','catalog','read'].includes(command)) {
295
+ const {id, options} = knowledgeArguments(command, flags);
296
+ const result = command === 'info' ? await knowledgeInfo(root) : command === 'catalog' ? await knowledgeCatalog(root, options) : await knowledgeRead(root,id,options);
297
+ log(JSON.stringify(result)); return result;
298
+ }
194
299
  if (
195
300
  flags.some((flag) => flag !== "--dry-run") ||
196
301
  (flags.length && command !== "init")
@@ -203,6 +308,6 @@ export async function run(args) {
203
308
  if (command && command !== "--help")
204
309
  throw new Error(`Unknown command: ${command}. Use --help.`);
205
310
  console.log(
206
- `Flood ${VERSION}\n\nnpx @flood-md/cli init [--dry-run] Connect a React or Next.js project\nnpx @flood-md/cli doctor Check packages and agent files\n\nComponents ship together in @flood-md/react; import the ones you need.\nUpdates: review the changelog and upgrade Flood packages together.\nhttps://flood.md`,
311
+ `Flood ${VERSION}\n\nnpx @flood-md/cli init [--dry-run] Connect a React or Next.js project\nnpx @flood-md/cli doctor Check packages and agent files\nnpx @flood-md/cli info Read project and knowledge versions as JSON\nnpx @flood-md/cli catalog Discover bounded knowledge metadata as JSON\nnpx @flood-md/cli read <id> Read one installed-version material as JSON\n\nComponents ship together in @flood-md/react; import the ones you need.\nUpdates: review the changelog and upgrade Flood packages together.\nhttps://flood.md`,
207
312
  );
208
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flood-md/cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "description": "Connect Flood to React and Next.js projects.",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "node": ">=22.12.0"
22
22
  },
23
23
  "dependencies": {
24
- "@flood-md/agent": "0.2.8"
24
+ "@flood-md/agent": "0.2.10"
25
25
  },
26
26
  "publishConfig": {
27
27
  "access": "public"