flowbook 0.2.2 → 0.2.4

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/dist/cli.js CHANGED
@@ -330,10 +330,10 @@ function resolveAgents(agentArg) {
330
330
  }
331
331
  function installFile(src, destDir, destFilename) {
332
332
  const dest = resolve3(destDir, destFilename);
333
- if (existsSync2(dest)) return false;
333
+ const existed = existsSync2(dest);
334
334
  mkdirSync(destDir, { recursive: true });
335
335
  copyFileSync(src, dest);
336
- return true;
336
+ return { action: existed ? "updated" : "installed" };
337
337
  }
338
338
  function installSkills(agentArg, global) {
339
339
  const agents = resolveAgents(agentArg);
@@ -343,32 +343,32 @@ function installSkills(agentArg, global) {
343
343
  console.error(" \u2717 Skill source file not found. Reinstall flowbook.");
344
344
  process.exit(1);
345
345
  }
346
- let skillCount = 0;
347
- let cmdCount = 0;
346
+ let installedSkills = 0;
347
+ let updatedSkills = 0;
348
+ let installedCmds = 0;
349
+ let updatedCmds = 0;
348
350
  for (const agent of agents) {
349
351
  const skillDir = resolve3(base, global ? agent.skill.global : agent.skill.project);
350
- if (installFile(skillSrc, skillDir, "SKILL.md")) {
351
- skillCount++;
352
- }
352
+ const skillResult = installFile(skillSrc, skillDir, "SKILL.md");
353
+ if (skillResult.action === "installed") installedSkills++;
354
+ else if (skillResult.action === "updated") updatedSkills++;
353
355
  if (agent.command) {
354
356
  const cmdSrc = getCommandSrc(agent.command.format);
355
357
  if (existsSync2(cmdSrc)) {
356
358
  const cmdDir = resolve3(base, global ? agent.command.global : agent.command.project);
357
- if (installFile(cmdSrc, cmdDir, "flowbook.md")) {
358
- cmdCount++;
359
- }
359
+ const cmdResult = installFile(cmdSrc, cmdDir, "flowbook.md");
360
+ if (cmdResult.action === "installed") installedCmds++;
361
+ else if (cmdResult.action === "updated") updatedCmds++;
360
362
  }
361
363
  }
362
364
  }
363
365
  const scope = global ? "global" : "project";
364
366
  const agentNames = agents.map((a) => a.name).join(", ");
365
- if (skillCount > 0 || cmdCount > 0) {
366
- if (skillCount > 0) console.log(` \u2713 Installed skill to ${skillCount} ${scope} director${skillCount > 1 ? "ies" : "y"}`);
367
- if (cmdCount > 0) console.log(` \u2713 Installed /flowbook command to ${cmdCount} ${scope} director${cmdCount > 1 ? "ies" : "y"}`);
368
- console.log(` Agents: ${agentNames}`);
369
- } else {
370
- console.log(` \u2713 Already installed for: ${agentNames}`);
371
- }
367
+ if (installedSkills > 0) console.log(` \u2713 Installed skill to ${installedSkills} ${scope} director${installedSkills > 1 ? "ies" : "y"}`);
368
+ if (updatedSkills > 0) console.log(` \u2713 Updated skill in ${updatedSkills} ${scope} director${updatedSkills > 1 ? "ies" : "y"}`);
369
+ if (installedCmds > 0) console.log(` \u2713 Installed /flowbook command to ${installedCmds} ${scope} director${installedCmds > 1 ? "ies" : "y"}`);
370
+ if (updatedCmds > 0) console.log(` \u2713 Updated /flowbook command in ${updatedCmds} ${scope} director${updatedCmds > 1 ? "ies" : "y"}`);
371
+ console.log(` Agents: ${agentNames}`);
372
372
  }
373
373
  function printSkillUsage() {
374
374
  console.log(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowbook",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "flowbook": "./dist/cli.js"
package/src/node/skill.ts CHANGED
@@ -135,12 +135,12 @@ function resolveAgents(agentArg: string): AgentConfig[] {
135
135
  return found;
136
136
  }
137
137
 
138
- function installFile(src: string, destDir: string, destFilename: string): boolean {
138
+ function installFile(src: string, destDir: string, destFilename: string): { action: "installed" | "updated" | "skipped" } {
139
139
  const dest = resolve(destDir, destFilename);
140
- if (existsSync(dest)) return false;
140
+ const existed = existsSync(dest);
141
141
  mkdirSync(destDir, { recursive: true });
142
142
  copyFileSync(src, dest);
143
- return true;
143
+ return { action: existed ? "updated" : "installed" };
144
144
  }
145
145
 
146
146
  export function installSkills(agentArg: string, global: boolean): void {
@@ -153,22 +153,24 @@ export function installSkills(agentArg: string, global: boolean): void {
153
153
  process.exit(1);
154
154
  }
155
155
 
156
- let skillCount = 0;
157
- let cmdCount = 0;
156
+ let installedSkills = 0;
157
+ let updatedSkills = 0;
158
+ let installedCmds = 0;
159
+ let updatedCmds = 0;
158
160
 
159
161
  for (const agent of agents) {
160
162
  const skillDir = resolve(base, global ? agent.skill.global : agent.skill.project);
161
- if (installFile(skillSrc, skillDir, "SKILL.md")) {
162
- skillCount++;
163
- }
163
+ const skillResult = installFile(skillSrc, skillDir, "SKILL.md");
164
+ if (skillResult.action === "installed") installedSkills++;
165
+ else if (skillResult.action === "updated") updatedSkills++;
164
166
 
165
167
  if (agent.command) {
166
168
  const cmdSrc = getCommandSrc(agent.command.format);
167
169
  if (existsSync(cmdSrc)) {
168
170
  const cmdDir = resolve(base, global ? agent.command.global : agent.command.project);
169
- if (installFile(cmdSrc, cmdDir, "flowbook.md")) {
170
- cmdCount++;
171
- }
171
+ const cmdResult = installFile(cmdSrc, cmdDir, "flowbook.md");
172
+ if (cmdResult.action === "installed") installedCmds++;
173
+ else if (cmdResult.action === "updated") updatedCmds++;
172
174
  }
173
175
  }
174
176
  }
@@ -176,13 +178,11 @@ export function installSkills(agentArg: string, global: boolean): void {
176
178
  const scope = global ? "global" : "project";
177
179
  const agentNames = agents.map((a) => a.name).join(", ");
178
180
 
179
- if (skillCount > 0 || cmdCount > 0) {
180
- if (skillCount > 0) console.log(` ✓ Installed skill to ${skillCount} ${scope} director${skillCount > 1 ? "ies" : "y"}`);
181
- if (cmdCount > 0) console.log(` ✓ Installed /flowbook command to ${cmdCount} ${scope} director${cmdCount > 1 ? "ies" : "y"}`);
182
- console.log(` Agents: ${agentNames}`);
183
- } else {
184
- console.log(` ✓ Already installed for: ${agentNames}`);
185
- }
181
+ if (installedSkills > 0) console.log(` ✓ Installed skill to ${installedSkills} ${scope} director${installedSkills > 1 ? "ies" : "y"}`);
182
+ if (updatedSkills > 0) console.log(` ✓ Updated skill in ${updatedSkills} ${scope} director${updatedSkills > 1 ? "ies" : "y"}`);
183
+ if (installedCmds > 0) console.log(` ✓ Installed /flowbook command to ${installedCmds} ${scope} director${installedCmds > 1 ? "ies" : "y"}`);
184
+ if (updatedCmds > 0) console.log(` Updated /flowbook command in ${updatedCmds} ${scope} director${updatedCmds > 1 ? "ies" : "y"}`);
185
+ console.log(` Agents: ${agentNames}`);
186
186
  }
187
187
 
188
188
  /** Used by init.ts — installs skills only (no commands) to all agents at project level */
@@ -191,14 +191,13 @@ export function installAllProjectSkills(): number {
191
191
  const skillSrc = getSkillSrc();
192
192
  if (!existsSync(skillSrc)) return 0;
193
193
 
194
- let installed = 0;
194
+ let count = 0;
195
195
  for (const agent of AGENTS) {
196
196
  const dir = resolve(cwd, agent.skill.project);
197
- if (installFile(skillSrc, dir, "SKILL.md")) {
198
- installed++;
199
- }
197
+ const result = installFile(skillSrc, dir, "SKILL.md");
198
+ if (result.action !== "skipped") count++;
200
199
  }
201
- return installed;
200
+ return count;
202
201
  }
203
202
 
204
203
  export function printSkillUsage(): void {
@@ -275,6 +275,37 @@ flowchart TD
275
275
  I[\Output\] %% Reverse parallelogram: response
276
276
  ```
277
277
 
278
+ #### Label Quoting Rules (MANDATORY)
279
+
280
+ Node labels containing special characters **MUST** be wrapped in double quotes to prevent Mermaid parse errors.
281
+
282
+ **Characters that REQUIRE quoting:**
283
+
284
+ | Character | Why it breaks | Unquoted (BROKEN) | Quoted (CORRECT) |
285
+ |-----------|--------------|-------------------|------------------|
286
+ | `()` | Conflicts with `([...])` stadium and `(...)` rounded shapes | `A([Agent run() Start])` | `A(["Agent run() Start"])` |
287
+ | `{}` | Conflicts with `{...}` diamond shape | `B{tokio::select!{}}` | `B{"tokio::select!{}"}` |
288
+ | `[]` | Conflicts with `[...]` rectangle shape | `C[arr[0] value]` | `C["arr[0] value"]` |
289
+ | `::` | Interpreted as Mermaid class/namespace syntax | `D[std::io::Error]` | `D["std::io::Error"]` |
290
+ | `#` | Interpreted as Unicode escape or comment | `E[Issue #42]` | `E["Issue #42"]` |
291
+ | `&` | Interpreted as HTML entity start | `F[A & B]` | `F["A & B"]` |
292
+
293
+ **Rule: When in doubt, quote it.** Quoting a label that doesn't need it causes no harm. Unquoted special characters WILL break rendering.
294
+
295
+ **Examples of correct quoting by node shape:**
296
+
297
+ ```mermaid
298
+ flowchart TD
299
+ A(["fn main() entry"]) %% Stadium with parens
300
+ B["process_data(input)"] %% Rectangle with parens
301
+ C{"is_valid(x)?"} %% Diamond with parens
302
+ D[["handle_error(err)"]] %% Subroutine with parens
303
+ E{{"validate(req)"}} %% Hexagon with parens
304
+ F["Config::new()"] %% Rectangle with double colon
305
+ ```
306
+
307
+ **NEVER generate unquoted labels containing `()`, `{}`, `[]`, `::`, `#`, or `&`.**
308
+
278
309
  #### Edge Labels
279
310
 
280
311
  ```mermaid
@@ -369,7 +400,7 @@ description: POST /api/auth/login — validates credentials and returns JWT toke
369
400
  ```mermaid
370
401
  flowchart TD
371
402
  A([POST /api/auth/login]) --> B[/Parse Request Body/]
372
- B --> C{{Validate Email & Password}}
403
+ B --> C{{"Validate Email & Password"}}
373
404
  C -->|Invalid| D[\400 Bad Request/]
374
405
  C -->|Valid| E[(Find User by Email)]
375
406
  E -->|Not Found| F[\401 Unauthorized/]
@@ -402,6 +433,16 @@ For each generated `.flow.md` file:
402
433
  1. Verify YAML frontmatter is valid (title, category present)
403
434
  2. Verify mermaid code block is properly fenced (``` mermaid ```)
404
435
  3. Verify mermaid syntax has no obvious errors (matched brackets, valid node IDs)
436
+ 4. **Special Character Validation (CRITICAL)**: Scan ALL node labels for unquoted special characters:
437
+ - `()` inside any node shape → MUST be quoted: `A(["label()"])` not `A([label()])`
438
+ - `{}` inside any node shape → MUST be quoted: `A{"label{}"}` not `A{label{}}`
439
+ - `[]` inside any node shape → MUST be quoted: `A["label[]"]` not `A[label[]]`
440
+ - `::` anywhere in labels → MUST be quoted: `A["std::io"]` not `A[std::io]`
441
+ - `#` anywhere in labels → MUST be quoted: `A["Issue #1"]` not `A[Issue #1]`
442
+ - `&` anywhere in labels → MUST be quoted: `A["A & B"]` not `A[A & B]`
443
+ - If ANY unquoted special characters are found, fix them BEFORE proceeding to build
444
+ 5. Verify all node IDs are unique within each diagram
445
+ 6. Verify subgraph labels don't contain special characters
405
446
 
406
447
  ### 5.2 Build Verification
407
448
 
@@ -483,6 +524,12 @@ Build: ✅ / ❌
483
524
  ### Mermaid syntax errors
484
525
  - **Brackets**: Every `[`, `{`, `(` must be closed
485
526
  - **Special characters in labels**: Wrap in double quotes: `A["User's Input"]`
527
+ - **Parentheses in labels** (MOST COMMON): `A([run() Start])` → Parse error. Fix: `A(["run() Start"])`
528
+ - **Double colons in labels**: `A[std::io::Error]` → Interpreted as class syntax. Fix: `A["std::io::Error"]`
529
+ - **Curly braces in labels**: `B{select!{}}` → Conflicts with diamond shape. Fix: `B{"select!{}"}`
530
+ - **Square brackets in labels**: `C[arr[0]]` → Conflicts with rectangle shape. Fix: `C["arr[0]"]`
531
+ - **Hash in labels**: `D[Issue #42]` → Unicode escape. Fix: `D["Issue #42"]`
532
+ - **Ampersand in labels**: `E[A & B]` → HTML entity. Fix: `E["A & B"]`
486
533
  - **Arrow syntax**: Use `-->` for solid, `-.->` for dotted, `==>` for thick
487
534
  - **Node ID reuse**: Each node ID must be unique per diagram. Reuse ID to reference same node.
488
535
  - **Subgraph naming**: Subgraph labels cannot contain special characters