faf-cli 7.12.1 → 7.13.1

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.
@@ -6,40 +6,130 @@
6
6
  */
7
7
  export declare const FAF_START = "<!-- faf:start -->";
8
8
  export declare const FAF_END = "<!-- faf:end -->";
9
+ /** Split into lines keeping each line's own terminator (\r\n, \r or \n). */
10
+ export declare function linesWithEnds(text: string): string[];
11
+ export declare const stripEnd: (line: string) => string;
9
12
  /**
10
- * Locate the faf-managed block in `text`: the first START marker line and the
11
- * first END marker line after it. Returns the char range covering both marker
12
- * lines (terminator of the END line excluded), or null when there is no
13
- * complete block.
13
+ * faf's marked range in `text`: in each reading, the last START line before
14
+ * the first END line after it (the innermost pair — text between an earlier
15
+ * START and the last one is kept), both outside every Markdown region. The
16
+ * two readings must find the same pair; otherwise there is no range. Returns
17
+ * the char range covering both lines (the END line's terminator excluded), or
18
+ * null. A leading BOM on line 1 stays outside the range. `isStart` and
19
+ * `isEnd` are asked about each line shown as text in at least one reading,
20
+ * once, in order, on the line with its terminator and that BOM removed. Past
21
+ * MAX_OPEN_CONTAINERS open containers the CommonMark reading stops, and there
22
+ * is no range (a pair found before that point still counts). Time grows with
23
+ * the text's length, not with how deep it nests.
24
+ */
25
+ export declare function findMarkedRange(text: string, isStart: (line: string) => boolean, isEnd: (line: string) => boolean): {
26
+ start: number;
27
+ end: number;
28
+ } | null;
29
+ /**
30
+ * Locate the faf-managed block in `text`: a START marker line and the first
31
+ * END marker line after it, both outside fenced and indented code, raw HTML
32
+ * blocks and multi-line HTML comments, under both readings (see
33
+ * {@link findMarkedRange}). Returns the char range covering both marker lines
34
+ * (terminator of the END line excluded), or null when there is no complete
35
+ * block both readings agree on.
14
36
  *
15
- * Markers are matched as WHOLE LINES at column 0, never as substrings. Substring
16
- * search was a real bug (7.1.4–7.11.0): renderAgentsMd quoted the marker tokens
17
- * in its own blockquote, so on every re-run `indexOf(end)` hit the quote, cut
18
- * the old block in half and appended its stale tail below the new block —
19
- * `faf export --agents` grew AGENTS.md by ~49 lines per run. The same happened
20
- * to users who documented the markers in a code fence above the block.
37
+ * Markers are matched as WHOLE LINES at column 0 — exactly the marker text,
38
+ * nothing after it — never as substrings. Substring search was a real bug
39
+ * (7.1.4–7.11.0): renderAgentsMd quoted the marker tokens in its own
40
+ * blockquote, so on every re-run `indexOf(end)` hit the quote, cut the old
41
+ * block in half and appended its stale tail below the new block — `faf
42
+ * export --agents` grew AGENTS.md by ~49 lines per run. The same happened to
43
+ * users who documented the markers in a code fence above the block.
21
44
  *
22
- * Two passes. The first skips START candidates inside fenced code, so a fenced
23
- * example is not mistaken for the block. Fence detection is a plain toggle and
24
- * Markdown has shapes it misreads (list-item fences, ```` around ```, a stray
25
- * unclosed fence), so if the first pass finds nothing the second ignores fences
26
- * entirely. A miss must never be silent: the caller treats "no block" as a
27
- * user file and prefixes — it does not overwrite.
45
+ * Two START lines before an END: the pair is the last START and that END.
46
+ * faf's own body never holds a column-0 START (see {@link wrapFafBlock}), so
47
+ * the text between the two STARTs is the user's and stays. A START with no
48
+ * END is not a block: the caller treats "no block" as a user file and
49
+ * prefixes — it never reclaims. faf's own block is always found again where
50
+ * faf put it (see {@link placeFafBlock}).
28
51
  */
29
52
  export declare function findFafBlock(text: string, start?: string, end?: string): {
30
53
  start: number;
31
54
  end: number;
32
55
  } | null;
56
+ /** No line of faf's own body leaves more block quotes, lists and list items
57
+ * open than this (as CommonMark reads it) — far below the reader's cap,
58
+ * MAX_OPEN_CONTAINERS, so the next run always reads the block to its end,
59
+ * quoted (one quote more) or not. See {@link capDepth}. */
60
+ export declare const BODY_MAX_CONTAINERS = 16;
61
+ /**
62
+ * `head`, faf's block (`wrapped`, from {@link wrapFafBlock}) and `tail` as one
63
+ * text in which {@link findFafBlock} finds the block exactly where it was put
64
+ * — so the next write updates it in place and never stacks a second one. A
65
+ * body that would not be found there (it leaves a region open in only one
66
+ * reading, or the text before it holds a raw HTML block the body continues)
67
+ * is quoted line by line instead. If even that is not found, it throws a
68
+ * SafePathError (`unplaceable`) naming `path` (the file being written):
69
+ * "<file>: faf could not place its block where the next run finds it again —
70
+ * faf left it unchanged" — one line, and nothing is written.
71
+ */
72
+ export declare function placeFafBlock(head: string, wrapped: string, tail: string, start?: string, end?: string, path?: string): string;
73
+ /** The managed block: START, the body (guarded — see guardBody), END. */
74
+ export declare function wrapFafBlock(block: string, start?: string, end?: string): string;
75
+ /** The file's new text: `existing` (null when there is no file) with `wrapped`
76
+ * as its managed block, placed where the next scan finds it again (see
77
+ * {@link placeFafBlock}; `path` names the file in its refusal). */
78
+ export declare function withFafBlock(existing: string | null, wrapped: string, start?: string, end?: string, path?: string): string;
79
+ /** Read a resolved path, or null when nothing is there yet. Only ENOENT reads
80
+ * as "no file"; any other error is thrown, so a file faf could not read is
81
+ * never treated as empty and written fresh. The text is decoded strictly: a
82
+ * file that is not UTF-8 (a UTF-16 file, cp1252 bytes) is refused
83
+ * (SafePathError `not-utf8`) and left as it is — see readUtf8. */
84
+ export declare function readIfPresent(path: string): string | null;
85
+ export interface InjectOptions {
86
+ /** The project folder the file must stay inside. Default: the file's own
87
+ * folder. Pass it when the file sits in a subfolder (`.github/…`), so a
88
+ * linked subfolder cannot carry the write out of the project. */
89
+ root?: string;
90
+ }
33
91
  /**
34
92
  * Non-destructively write a faf-managed block into a file.
35
93
  *
36
94
  * - file does not exist → create it containing just the block
37
95
  * - file has the markers → replace ONLY the content between them (update in place)
38
- * - legacy faf file → metastamp-led, no marker lines: reclaim in place
39
- * - file exists, no block → PREFIX the block; everything the user wrote is preserved
96
+ * - anything else → PREFIX the block; everything already there is preserved
97
+ * (a leading BOM stays at byte 0)
98
+ *
99
+ * faf replaces only text it can prove it wrote: what sits between its own
100
+ * marker lines. A file with no marker lines is never reclaimed, whatever it
101
+ * starts or ends with. Idempotent: re-running updates the managed block in
102
+ * place and never duplicates it. Enhance, never replace.
40
103
  *
41
- * Idempotent: re-running updates the managed block in place and never duplicates
42
- * it or touches a byte the user owns. faf owns what's between the markers; the
43
- * user owns everything else. Enhance, never replace.
104
+ * The file is resolved inside its project first: a link that leads outside,
105
+ * a dangling link, a link to a file with another name (CLAUDE.md → README.md)
106
+ * and anything in `.git` are refused (SafePathError) and nothing is written; a
107
+ * link to a file of the same name, or between AI context files (CLAUDE.md →
108
+ * AGENTS.md), is written through and stays a link. A file that is not UTF-8
109
+ * is refused and left as it is. The write is atomic — a failure leaves the
110
+ * original exactly as it was — and is refused if the file changed on disk
111
+ * after faf read it. A block faf cannot place where its next run finds it
112
+ * again is refused too (SafePathError `unplaceable`), and nothing is written.
113
+ */
114
+ export declare function injectFafBlock(path: string, block: string, start?: string, end?: string, opts?: InjectOptions): void;
115
+ /**
116
+ * The one line the CLI prints when faf's block goes on top of a file that has
117
+ * no block of its own but holds older faf text — or null:
118
+ * - the file's first line is faf's old metastamp (`<!-- faf: … -->`): faf
119
+ * never reclaims such a file (it cannot prove it wrote the text), so the
120
+ * old faf text stays below the new block;
121
+ * - a whole-line START marker sits inside a code fence, a raw HTML block
122
+ * or an HTML comment (in either reading — see {@link findFafBlock}): the
123
+ * older block there is an example to faf, and stays below the new one;
124
+ * - a whole-line START marker sits past more than MAX_OPEN_CONTAINERS
125
+ * nested lists or quotes: faf did not read that far, and the older block
126
+ * stays below the new one.
127
+ * `label` names the file (`CLAUDE.md`). `existing` is the file's text before
128
+ * the write (null when there was none). faf-mcp and claude-faf-mcp print the
129
+ * same line through this export.
44
130
  */
45
- export declare function injectFafBlock(path: string, block: string, start?: string, end?: string): void;
131
+ export declare function legacyStampNote(label: string, existing: string | null, start?: string, end?: string): string | null;
132
+ /** {@link legacyStampNote} for the file at `path`, read the way
133
+ * injectFafBlock reads it — before the write. Null when there is no note, or
134
+ * when the file cannot be read (the write itself then says why). */
135
+ export declare function legacyStampNoteAt(path: string, label: string, start?: string, end?: string, opts?: InjectOptions): string | null;
@@ -1,7 +1,24 @@
1
1
  import type { FafData, ScoreResult } from '../core/types.js';
2
2
  /** Render project.faf data + its score into a self-contained HTML string. */
3
3
  export declare function renderProjectHtml(data: FafData, result: ScoreResult, fafPath?: string): string;
4
- /** Write project.html beside project.faf (repo root). */
5
- export declare function writeProjectHtml(dir: string, data: FafData, result: ScoreResult, fafPath?: string): void;
4
+ /** True when `bytes` are a project.html faf rendered: a line that is faf's
5
+ * description meta (every faf release has written it). */
6
+ export declare function hasProjectHtmlMark(bytes: Uint8Array): boolean;
7
+ /** Options for {@link writeProjectHtml}. */
8
+ export interface ProjectHtmlWriteOptions {
9
+ /** Replace a project.html faf cannot prove it wrote — no faf mark, edited
10
+ * since faf wrote it, or from before 7.13 — the explicit overwrite
11
+ * (`--force`). Default: it is refused and left as it is. */
12
+ force?: boolean;
13
+ }
14
+ /** Write project.html beside project.faf (repo root) — atomically, and never
15
+ * through a link that leaves `dir` or dangles (SafePathError). The page
16
+ * carries faf's render hash (`<meta name="faf-render" content="sha256:…">`,
17
+ * the hash of the page without that line). A project.html already there is
18
+ * replaced only when it is byte for byte what faf last wrote (its hash still
19
+ * fits); a page edited since, a hand-written page, or a page from before 7.13
20
+ * that is not exactly faf's render of `data` is refused (SafePathError
21
+ * `not-owned`) and left byte for byte, unless `force`. */
22
+ export declare function writeProjectHtml(dir: string, data: FafData, result: ScoreResult, fafPath?: string, write?: ProjectHtmlWriteOptions): void;
6
23
  /** @deprecated Use {@link renderProjectHtml}. Removed in the next major. */
7
24
  export declare const generateProjectHtml: typeof renderProjectHtml;
@@ -63,10 +63,59 @@ export declare function registryName(data: FafData): string;
63
63
  * omitted rather than shipped invalid — GitHub's registry then derives a name
64
64
  * from the namespace, so set `project.title` to control the display. */
65
65
  export declare function registryTitle(data: FafData): string | undefined;
66
+ /** True when `bytes` are a Server Card faf wrote: JSON carrying the FAF
67
+ * context-block at `_meta["one.faf/context"]`, as every card faf has
68
+ * written does. */
69
+ export declare function hasServerCardMark(bytes: Uint8Array): boolean;
70
+ /** True when `bytes` are a registry `server.json` carrying faf's identity:
71
+ * `_meta[REGISTRY_PUBLISHER_KEY]["one.faf/context"]`. */
72
+ export declare function hasRegistryMark(bytes: Uint8Array): boolean;
73
+ /** Options for the card writers ({@link writeServerCard}, `faf cards`). */
74
+ export interface CardWriteOptions {
75
+ /** Replace a card faf cannot prove it wrote — no faf mark, edited since
76
+ * faf wrote it, or from before 7.13 — the explicit overwrite (`--force`).
77
+ * Default: such a file is refused and left as it is. */
78
+ force?: boolean;
79
+ }
66
80
  /** Write the Server Card to a `server-card` file. Returns the path.
67
81
  * Per experimental-ext-server-card#22 the reserved location is
68
82
  * `<streamable-http-url>/server-card` (no longer `.well-known`); serve the
69
- * emitted file there as `application/mcp-server-card+json`. */
70
- export declare function writeServerCard(dir: string, data: FafData, opts?: ServerCardOptions): string;
83
+ * emitted file there as `application/mcp-server-card+json`.
84
+ *
85
+ * The card carries faf's render hash at `_meta["one.faf/render"]` (the hash
86
+ * of the card without that key; the Server Card schema leaves `_meta` open
87
+ * for namespaced keys). A `server-card` already there is replaced only when
88
+ * it is byte for byte what faf last wrote (its hash still fits); a card edited
89
+ * since, a hand-written card, or a card from before 7.13 that is not exactly
90
+ * faf's render of `data` is refused (SafePathError `not-owned`) and left byte
91
+ * for byte, unless `force`. The write is atomic and never goes through a link
92
+ * that leaves `dir` or dangles. */
93
+ export declare function writeServerCard(dir: string, data: FafData, opts?: ServerCardOptions, write?: CardWriteOptions): string;
94
+ /** The identity faf owns in a registry `server.json`. */
95
+ export interface ServerJsonIdentity {
96
+ /** The reverse-DNS registry name ({@link registryName}). */
97
+ name: string;
98
+ /** The display title ({@link registryTitle}); when undefined the file's own title is kept. */
99
+ title?: string;
100
+ /** A version to set (`--set-version`); when undefined the file's own is kept. */
101
+ version?: string;
102
+ /** The `_meta` faf writes ({@link registryMeta}). */
103
+ meta: Record<string, unknown>;
104
+ }
105
+ /**
106
+ * Put faf's identity into the text of a registry `server.json`, changing
107
+ * nothing else: the `name` value, the `title` (only when faf has one — the
108
+ * file's own title is kept otherwise), a `version` asked for, and the keys of
109
+ * faf's context-block under `_meta[REGISTRY_PUBLISHER_KEY]["one.faf/context"]`.
110
+ * Each is a text edit of that one value, or the key added when missing; no
111
+ * field is deleted, and every other byte — key order, a 20-digit number, an
112
+ * array on one line, spacing, CRLF — stays as it was. Throws a JsonEditError,
113
+ * changing nothing, when the text cannot be edited that way (not valid JSON,
114
+ * not an object, a key repeated on the way, a `_meta` that is not an object).
115
+ */
116
+ export declare function patchServerJson(text: string, identity: ServerJsonIdentity): {
117
+ text: string;
118
+ changed: boolean;
119
+ };
71
120
  /** @deprecated Use {@link buildServerCard}. Removed in the next major. */
72
121
  export declare const generateServerCard: typeof buildServerCard;
@@ -1,11 +1,14 @@
1
1
  import type { KernelScoreResult, FafbInfo } from '../core/types.js';
2
- /** Score a .faf YAML string (21 base slots) */
2
+ /** Score a .faf YAML string (21 base slots), as the file is. A typed None /
3
+ * N/A / not applicable at a slot is an empty slot, as in every engine. A
4
+ * leading BOM is not scored (`faf auto` and `faf score` read a BOM file). */
3
5
  export declare function score(yaml: string): KernelScoreResult;
4
- /** Score a .faf YAML string (33 enterprise slots) */
6
+ /** Score a .faf YAML string (33 enterprise slots), as the file is (a leading BOM is not scored). */
5
7
  export declare function scoreEnterprise(yaml: string): KernelScoreResult;
6
- /** Validate .faf YAML */
8
+ /** Validate .faf YAML (a leading BOM is left out, as in {@link score}: `faf check` reads a BOM file). */
7
9
  export declare function validate(yaml: string): boolean;
8
- /** Compile .faf YAML to FAFb binary */
10
+ /** Compile .faf YAML to FAFb binary (a leading BOM is not compiled, as in
11
+ * {@link score}: `faf compile` and `faf refresh` read a BOM file). */
9
12
  export declare function compile(yaml: string): Uint8Array;
10
13
  /** Decompile FAFb binary to JSON info */
11
14
  export declare function decompile(bytes: Uint8Array): FafbInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faf-cli",
3
- "version": "7.12.1",
3
+ "version": "7.13.1",
4
4
  "description": "Persistent AI context + memory — .faf and .fafm, IANA-registered. Anthropic-merged.",
5
5
  "type": "module",
6
6
  "icon": "https://faf.one/orange-smiley.svg",
@@ -31,10 +31,11 @@
31
31
  "compile:all": "bun build src/cli.ts --compile --bytecode --minify --target=bun-darwin-arm64 --outfile faf-darwin-arm64 && bun build src/cli.ts --compile --bytecode --minify --target=bun-darwin-x64 --outfile faf-darwin-x64 && bun build src/cli.ts --compile --bytecode --minify --target=bun-linux-x64 --outfile faf-linux-x64 && bun build src/cli.ts --compile --bytecode --minify --target=bun-windows-x64 --outfile faf-windows-x64.exe",
32
32
  "test": "bun test --timeout=120000",
33
33
  "test:watch": "bun test --watch --timeout=120000",
34
- "lint": "eslint src/**/*.ts",
34
+ "lint": "eslint 'src/**/*.ts'",
35
35
  "format": "prettier --write 'src/**/*.ts'",
36
36
  "check:no-hardcode": "! grep -rEln '/(Users|home/runner|private/var)/' dist/ || (echo '❌ Hardcoded build-machine path leaked into dist — see grep output above. Externalize the offending dep in the build script.'; exit 1)",
37
- "prepublishOnly": "bun run build && bun run check:no-hardcode"
37
+ "check:engines": "node scripts/check-engines.mjs",
38
+ "prepublishOnly": "bun run check:engines && bun run build && bun run check:no-hardcode"
38
39
  },
39
40
  "keywords": [
40
41
  "faf",
@@ -94,7 +95,7 @@
94
95
  "picomatch": "^4.0.5"
95
96
  },
96
97
  "engines": {
97
- "node": ">=18.0.0"
98
+ "node": ">=22.0.0"
98
99
  },
99
100
  "publishConfig": {
100
101
  "access": "public"
package/project.faf CHANGED
@@ -1,8 +1,8 @@
1
1
  faf_version: "3.0"
2
2
  project:
3
3
  name: faf-cli
4
- version: "7.12.1"
5
- goal: "CLI for IANA-registered `.faf` + `.fafm` — context DNA and portable agent memory. TypeScript, Bun-native since v6. package faf-cli v7.12.1 The Open Renderers Edition."
4
+ version: "7.13.1"
5
+ goal: "CLI for IANA-registered `.faf` + `.fafm` — context DNA and portable agent memory. TypeScript, Bun-native since v6. package faf-cli v7.13.1 The Co-Author Edition."
6
6
  main_language: TypeScript
7
7
  type: cli # found: package.json bin
8
8
  # Commands + key_files feed `faf export --agents` (hand values win over detection).
@@ -58,7 +58,7 @@ human_context:
58
58
  what: Persistent AI Context Standard — project DNA for AI. IANA-registered. Anthropic-merged.
59
59
  why: Eliminates 91% context re-discovery tax — define once, AI remembers forever
60
60
  where: npm registry, Homebrew, GitHub
61
- when: Production since September 2025; Bun-native since v6; current package 7.12.1
61
+ when: Production since September 2025; Bun-native since v6; current package 7.13.1
62
62
  how: bunx faf-cli auto, then project.faf versions with your code — faf show renders it human-visible
63
63
  monorepo:
64
64
  packages_count: slotignored
@@ -68,7 +68,7 @@ monorepo:
68
68
  remote_cache: slotignored
69
69
  surfaces:
70
70
  # Distribution map — the machine-checkable form of human_context.where. Score-neutral.
71
- auto: # derived from manifests; regenerate with `faf surface --sync`
71
+ auto: # derived from manifests; re-derive with the /surface skill
72
72
  - { type: npm, id: faf-cli, source: "package.json#name" }
73
73
  - { type: github, id: Wolfe-Jam/faf-cli, source: "package.json#repository" }
74
74
  declared: # hand-authored: surfaces the manifests don't carry + targets
@@ -89,3 +89,10 @@ ai_instructions:
89
89
  testing: WJTTC — zero errors always; bun test green before any ship.
90
90
  runtime: Bun-native since v6, TypeScript strict, Rust→WASM scoring kernel.
91
91
  releases: Atomic via /pubpro — bump, verify, tag, publish in one motion.
92
+ tech_stack:
93
+ - TypeScript
94
+ - Bun
95
+ - commander
96
+ - faf-scoring-kernel
97
+ - open
98
+ - yaml