@aisystemresources/emdee 0.3.2 → 0.3.3

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,14 @@
1
+ {
2
+ "name": "aisystemresources",
3
+ "owner": {
4
+ "name": "AI System Resources",
5
+ "url": "https://emdee.tech"
6
+ },
7
+ "plugins": [
8
+ {
9
+ "name": "emdee",
10
+ "source": "./",
11
+ "description": "A markdown knowledge graph shared by humans and their AI agents. Bundles the emdee.tech MCP server, the emdee CLI, and 4 Claude Code skills that teach vault conventions + workflows."
12
+ }
13
+ ]
14
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "emdee",
3
- "displayName": "Emdee — Local-first knowledge graph",
4
- "description": "Local-first knowledge graph backed by plain markdown. Bundles the emdee.tech MCP server, the emdee CLI, and 4 Claude Code skills that teach vault conventions + workflows.",
5
- "version": "0.2.2",
3
+ "displayName": "Emdee — Markdown knowledge graph for humans and agents",
4
+ "description": "A markdown knowledge graph shared by humans and their AI agents. Bundles the emdee.tech MCP server, the emdee CLI, and 4 Claude Code skills that teach vault conventions + workflows.",
5
+ "version": "0.3.2",
6
6
  "author": {
7
7
  "name": "AI System Resources",
8
8
  "url": "https://emdee.tech"
package/bin/emdee.js CHANGED
@@ -664,6 +664,33 @@ program
664
664
  shellRead("list-summary-drift", opts, extra);
665
665
  });
666
666
 
667
+ program
668
+ .command("lint-doc")
669
+ .description("Audit one doc for quality defects (missing preamble, asymmetric edges, sibling assocs, etc).")
670
+ .requiredOption("--path <path>", "Vault doc path")
671
+ .option("-d, --docs <dir>", "docs directory (local mode)")
672
+ .option("--remote", "Route through emdee.tech")
673
+ .option("--json", "Machine-parseable output")
674
+ .action((opts) => {
675
+ const extra = argsFromOpts(opts, { path: "--path", remote: "--remote", json: "--json" });
676
+ shellRead("lint-doc", opts, extra);
677
+ });
678
+
679
+ program
680
+ .command("lint-vault")
681
+ .description("Batch-lint every doc in the vault. Returns aggregated warnings by code + per-doc punch list.")
682
+ .option("--prefix <p>", "Path prefix filter (scope to a subtree)")
683
+ .option("--limit <n>", "Max docs in the punch list (default: all)")
684
+ .option("-d, --docs <dir>", "docs directory (local mode)")
685
+ .option("--remote", "Route through emdee.tech")
686
+ .option("--json", "Machine-parseable output")
687
+ .action((opts) => {
688
+ const extra = argsFromOpts(opts, {
689
+ prefix: "--prefix", limit: "--limit", remote: "--remote", json: "--json",
690
+ });
691
+ shellRead("lint-vault", opts, extra);
692
+ });
693
+
667
694
  // -----------------------------------------------------------------------
668
695
  // Deferred parity gap closers (post-SPRINT-091): 4 remaining tool verbs.
669
696
  // -----------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisystemresources/emdee",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Local-first document management with markdown rendering, knowledge graph, and MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,19 +2,19 @@
2
2
  name: emdee-conventions
3
3
  description: |
4
4
  Use whenever reading or writing an EMDEE vault. Covers the 5-node OS layer,
5
- doc shape (H1 + blockquote + relationship sections), edge discipline, tool
6
- selection (CLI vs MCP, atomic multi-side writes), and path conventions
7
- (UPPERCASE filenames, lowercase folders, hub-next-to-folder). Load this once
8
- and every subsequent vault operation lands correctly the first time.
5
+ doc shape (H1 + blockquote + relationship sections), edge discipline, CLI
6
+ tool selection (atomic multi-side writes), and path conventions (UPPERCASE
7
+ filenames, lowercase folders, hub-next-to-folder). Load this once and every
8
+ subsequent vault operation lands correctly the first time.
9
9
  ---
10
10
 
11
11
  # EMDEE conventions — always-loaded
12
12
 
13
- You are working inside an Emdee vault. Every doc is plain markdown. Humans browse it through a Next.js renderer at [emdee.tech](https://emdee.tech); agents (Claude Code, Claude.ai, Cursor) read and write the same files through an MCP server OR the `emdee` CLI. The vault is the source of truth — anything you say must trace back to a file the user wrote.
13
+ You are working inside an Emdee vault. Every doc is plain markdown. Humans browse it through a Next.js renderer at [emdee.tech](https://emdee.tech); you read and write the same files through the `emdee` CLI. The vault is the source of truth — anything you say must trace back to a file the user wrote.
14
14
 
15
15
  ## The 5-node OS layer (virtual system nodes)
16
16
 
17
- Every vault has exactly 5 canonical top-level docs plus one owner node. **These 5 are virtual** — they appear in every read (`emdee list`, `get_doc`, MCP responses) without being on disk. Edit them and your version wins.
17
+ Every vault has exactly 5 canonical top-level docs plus one owner node. **These 5 are virtual** — they appear in every read (`emdee list`, `emdee get-doc`, etc.) without being on disk. Edit them and your version wins.
18
18
 
19
19
  | Node | Purpose |
20
20
  |---|---|
@@ -50,13 +50,13 @@ Every vault has exactly 5 canonical top-level docs plus one owner node. **These
50
50
  Freeform content.
51
51
  ```
52
52
 
53
- Rules the lint enforces (see `lint_doc(path)` or `emdee patch-section --gate-on <code>`):
53
+ Rules the lint enforces (see `emdee lint-doc --path X` or `emdee patch-section --gate-on <code>`):
54
54
 
55
55
  - **One parent per doc.** `## Child of` should have exactly one bullet. Multiple parents → demote secondaries to `## Associated with`.
56
56
  - **No sibling associations.** Docs that share a parent are already related through it. `## Associated with` is for cross-tree links (project↔person, sprint↔learning).
57
57
  - **Reciprocal edges.** If A's `## Parent of` lists `[[B]]`, B's `## Child of` must list `[[A]]`. Asymmetric edges fire `asymmetric_parent_edge` / `asymmetric_child_edge` warnings.
58
58
  - **First wiki-link = declared edge.** `* [[TARGET]] — prose about the link`. The bullet's leading link is the edge; other links in the bullet are inline mentions only.
59
- - **Sibling order is derived, never declared.** `get_neighbors` returns `prev_sibling` / `next_sibling` computed from the parent's `## Parent of` order. Do NOT add `[[next-node]]` / `[[prev-node]]` bullets.
59
+ - **Sibling order is derived, never declared.** `emdee get-neighbors` returns `prev_sibling` / `next_sibling` computed from the parent's `## Parent of` order. Do NOT add `[[next-node]]` / `[[prev-node]]` bullets.
60
60
 
61
61
  ## Path conventions
62
62
 
@@ -66,22 +66,25 @@ Rules the lint enforces (see `lint_doc(path)` or `emdee patch-section --gate-on
66
66
 
67
67
  ## Tool selection
68
68
 
69
- Prefer CLI over MCP for token efficiency:
69
+ Everything runs through the `emdee` CLI. `--remote` routes through the authenticated cloud vault at emdee.tech; omit for local mode.
70
70
 
71
71
  ```bash
72
- emdee list --remote # 40× cheaper than list_docs MCP call
73
- emdee get-doc --path X --remote --full # cheaper than get_doc MCP call
72
+ emdee list --remote # top-level vault paths
73
+ emdee get-doc --path X --remote --full # doc body + per-section hashes
74
+ emdee search --query "keyword" --remote
74
75
  ```
75
76
 
76
- All 16 tool verbs are available as `emdee <verb>`. Same guards, same errors, same semantics as the MCP tool of the same name (hyphenated). Everything supports `--remote` (cloud) or defaults local.
77
+ Reads: `list`, `list-docs`, `get-doc`, `get-summary`, `get-neighbors`, `get-context`, `read-doc-section`, `search`, `list-summary-drift`, `drift-batch`, `lint-doc`, `get-image`.
78
+
79
+ Writes: `patch-section`, `patch-preamble`, `append-section`, `append-doc`, `create-child`, `add-association`, `move-doc`, `rename-doc`, `trash-doc`, `restore-doc`, `delete-doc`, `write-doc`, `write-doc-preview`, `distill-doc`, `materialize-subgroup`, `split-doc`.
77
80
 
78
81
  For writes, always prefer the atomic multi-side variants over raw section patches:
79
82
 
80
83
  | Instead of… | Use… |
81
84
  |---|---|
82
- | Two `patch_section` calls (child's Child of + parent's Parent of) | `emdee create-child` |
83
- | Two `patch_section` calls on both docs' Associated with | `emdee add-association` |
84
- | Three `patch_section` calls to reparent | `emdee move-doc` |
85
+ | Two `patch-section` calls (child's Child of + parent's Parent of) | `emdee create-child` |
86
+ | Two `patch-section` calls on both docs' Associated with | `emdee add-association` |
87
+ | Three `patch-section` calls to reparent | `emdee move-doc` |
85
88
  | Search+replace across every doc that references a title | `emdee rename-doc` |
86
89
 
87
90
  The atomic variants take care of edge discipline (hard-refuse on sibling-assoc-redundant, would-duplicate-hierarchy) and rollback semantics.
@@ -95,12 +98,12 @@ Every destructive write (`patch-section`, `patch-preamble`, `move-doc`) requires
95
98
  ## HARD RULES you'll trip if you're not careful
96
99
 
97
100
  1. **Any Supabase `.select()` reading > 1000 rows must paginate.** Use `.range(offset, offset + PAGE - 1)` in a loop. `.range()` alone doesn't lift the server-side 1000-row cap.
98
- 2. **`patch_section` on a `## Child of` requires patching the new parent's `## Parent of` in the same turn.** Otherwise you leave an asymmetric edge. Or just use `move-doc`.
99
- 3. **New / modified MCP tools require an e2e spec in the same PR.** Test the tool's behaviour end-to-end via a real `ToolContext` + temp filesystem.
101
+ 2. **`patch-section` on a `## Child of` requires patching the new parent's `## Parent of` in the same turn.** Otherwise you leave an asymmetric edge. Or just use `move-doc`.
102
+ 3. **Any change to a tool's behaviour (CLI verb or MCP surface) requires an e2e spec in the same PR.** Test end-to-end via a real `ToolContext` + temp filesystem.
100
103
  4. **Migrations touch `supabase/migrations/**` and are NEVER auto-merged.** Human review required.
101
104
 
102
105
  ## When in doubt
103
106
 
104
- - `emdee lint-doc --path X` — surface every warning code the doc trips
105
- - `emdee get-doc --path X` — see current state including per-section hashes
106
- - Read the source of truth: `edmund/projects/emdee_os/LEARNINGS.md` in the vault
107
+ - `emdee lint-doc --path X --remote` — surface every warning code the doc trips
108
+ - `emdee get-doc --path X --remote` — see current state including per-section hashes
109
+ - Read the source of truth: `emdee get-doc --path "edmund/projects/emdee_os/LEARNINGS.md" --remote --full`
@@ -9,7 +9,7 @@ description: |
9
9
 
10
10
  # emdee-describe-image — rename + summarise uploaded images
11
11
 
12
- When you see an EMDEE image doc that hasn't been described yet, run this workflow. It uses the existing `get_image` + `rename_doc` + `patch_preamble` MCP tools (or their `emdee` CLI equivalents) no new server-side dependencies.
12
+ When you see an EMDEE image doc that hasn't been described yet, run this workflow. Pure `emdee` CLI no MCP calls. Every command routes through the authenticated cloud vault via `--remote`.
13
13
 
14
14
  ## Trigger patterns
15
15
 
@@ -22,17 +22,11 @@ Any vault doc where:
22
22
 
23
23
  ### 1. Read the image
24
24
 
25
- ```
26
- get_image(doc_path=<PATH>)
27
- ```
28
-
29
- Or via CLI (when SPRINT-091 chunk 4 lands `get-image`):
30
-
31
- ```
32
- emdee get-image --path <PATH> --remote
25
+ ```bash
26
+ emdee get-image --doc-path <PATH> --out /tmp/img.png --remote
33
27
  ```
34
28
 
35
- The MCP tool returns the image as a visual content block you can see it.
29
+ Then read `/tmp/img.png` you'll see it as a visual content block and can describe what's in it.
36
30
 
37
31
  ### 2. Compose
38
32
 
@@ -43,11 +37,11 @@ The MCP tool returns the image as a visual content block — you can see it.
43
37
 
44
38
  ### 3. Rename
45
39
 
46
- ```
40
+ ```bash
47
41
  emdee rename-doc --old-path <PATH> --new-title <TITLE> --remote
48
42
  ```
49
43
 
50
- The tool atomically:
44
+ Atomically:
51
45
  - Rewrites the H1
52
46
  - Moves the file (default: same folder, `<TITLE>.md`)
53
47
  - Updates every `[[<old title>]]` wiki-link across the vault
@@ -56,13 +50,13 @@ The tool atomically:
56
50
 
57
51
  Fetch the fresh preamble hash:
58
52
 
59
- ```
53
+ ```bash
60
54
  emdee get-doc --path <NEW-PATH> --remote --json
61
55
  ```
62
56
 
63
57
  Take `preamble.content_hash` from the response, then:
64
58
 
65
- ```
59
+ ```bash
66
60
  emdee patch-preamble --path <NEW-PATH> \
67
61
  --body "> <your 15-30 word summary>" \
68
62
  --expected-hash <hash> --remote
@@ -72,8 +66,8 @@ emdee patch-preamble --path <NEW-PATH> \
72
66
 
73
67
  If several images need describing:
74
68
 
75
- ```
76
- emdee list-docs --prefix "IMAGES/" --remote | \
69
+ ```bash
70
+ emdee list-docs --prefix "IMAGES/" --remote --format text | \
77
71
  xargs -I{} emdee get-summary --path {} --remote --format text
78
72
  ```
79
73
 
@@ -81,7 +75,7 @@ Filter for `_description pending_` in the output, then run the 4-step workflow p
81
75
 
82
76
  ## Failure modes
83
77
 
84
- - **Title collision** — `rename_doc` returns `title_conflict`. Pick a more specific title (add a subject qualifier).
78
+ - **Title collision** — `rename-doc` returns `title_conflict`. Pick a more specific title (add a subject qualifier).
85
79
  - **`_description pending_` was already replaced** — someone else already ran this. `emdee get-doc` will show the new summary; skip.
86
80
  - **Image is illegible** — set the summary to `> Illegible / unable to describe from image alone.` and flag it in the user-facing report so they can annotate manually.
87
81
 
@@ -107,7 +107,7 @@ Load the conventions skill (if not already loaded) and let it take over.
107
107
  ## What to avoid
108
108
 
109
109
  - **Don't dump the full conventions in step 2.** The user is trying to see one thing work. Ship them from zero → seeing their first graph node in under 5 minutes. Depth comes after.
110
- - **Don't force `create_child` before they know what to create.** The step-3 question ("what do you want to track first?") is critical. If they say "I don't know yet," don't create a doc — instead, suggest they browse [emdee.tech](https://emdee.tech) in incognito to see the public demo vault. Come back when they have an answer.
110
+ - **Don't force `create-child` before they know what to create.** The step-3 question ("what do you want to track first?") is critical. If they say "I don't know yet," don't create a doc — instead, suggest they browse [emdee.tech](https://emdee.tech) in incognito to see the public demo vault. Come back when they have an answer.
111
111
  - **Don't reference SPRINTs / LOGS / LEARNINGS in onboarding.** Those are for later. Zero-to-one first.
112
112
 
113
113
  ## Success signal
@@ -9,7 +9,7 @@ description: |
9
9
 
10
10
  # emdee-summariser — refresh drifting doc summaries
11
11
 
12
- Docs in EMDEE carry a one-line `> blockquote` summary right below the H1. That summary is what routing sees — search, `get_summary`, cheap enumeration all pivot on it. When the body drifts (new sections, refined framing, changed scope), the summary should catch up. This skill runs that refresh.
12
+ Docs in EMDEE carry a one-line `> blockquote` summary right below the H1. That summary is what routing sees — `emdee search`, `emdee get-summary`, cheap enumeration all pivot on it. When the body drifts (new sections, refined framing, changed scope), the summary should catch up. This skill runs that refresh.
13
13
 
14
14
  ## Trigger patterns
15
15
 
@@ -25,6 +25,8 @@ import { search } from "../lib/mcp/tools/search";
25
25
  import { readDocSection } from "../lib/mcp/tools/read_doc_section";
26
26
  import { listDocs } from "../lib/mcp/tools/list_docs";
27
27
  import { listSummaryDrift } from "../lib/mcp/tools/list_summary_drift";
28
+ import { lintDoc } from "../lib/mcp/tools/lint_doc";
29
+ import { lintVault } from "../lib/mcp/tools/lint_vault";
28
30
  import { getImage } from "../lib/mcp/tools/get_image";
29
31
  import { writeFileSync } from "node:fs";
30
32
 
@@ -175,6 +177,29 @@ const READ_VERBS: Record<string, ReadVerb> = {
175
177
  return args;
176
178
  },
177
179
  },
180
+ "lint-doc": {
181
+ toolName: "lint_doc",
182
+ toolFn: lintDoc as unknown as ToolFn,
183
+ parse: { ...COMMON, path: { type: "string" } },
184
+ buildArgs: (v) => ({ path: asString(v.path) }),
185
+ },
186
+ "lint-vault": {
187
+ toolName: "lint_vault",
188
+ toolFn: lintVault as unknown as ToolFn,
189
+ parse: {
190
+ ...COMMON,
191
+ prefix: { type: "string" },
192
+ limit: { type: "string" },
193
+ },
194
+ buildArgs: (v) => {
195
+ const args: Record<string, unknown> = {};
196
+ const prefix = optionalString(v.prefix);
197
+ if (prefix) args.prefix = prefix;
198
+ const limit = optionalString(v.limit);
199
+ if (limit) args.limit = Number(limit);
200
+ return args;
201
+ },
202
+ },
178
203
  };
179
204
 
180
205
  function formatReadOutput(result: unknown, wantJson: boolean): string {
@@ -17,6 +17,7 @@ export { renameDoc } from "./rename_doc";
17
17
  export { patchPreamble } from "./patch_preamble";
18
18
  export { appendDoc } from "./append_doc";
19
19
  export { lintDoc } from "./lint_doc";
20
+ export { lintVault } from "./lint_vault";
20
21
  export { distillDoc } from "./distill_doc";
21
22
  export { createChild } from "./create_child";
22
23
  export { addAssociation } from "./add_association";
@@ -0,0 +1,59 @@
1
+ import { loadVaultIndex } from "./vault";
2
+ import { lintDocContent, type LintWarning } from "./lint";
3
+ import { buildLintVaultContext } from "./lint_doc";
4
+ import type { ToolContext } from "./types";
5
+
6
+ // SPRINT-101: batch lint the caller's entire vault in one shot.
7
+ //
8
+ // Reuses the vault index's in-memory `content` (already fetched by
9
+ // `loadVaultIndex`) so we run every per-doc + cross-doc rule without a
10
+ // single extra Storage/cache round-trip. On a ~1500-doc vault this is
11
+ // milliseconds, not seconds.
12
+ //
13
+ // Response shape:
14
+ // { scanned, with_warnings, warnings_by_code, docs: [{path, warnings[]}] }
15
+ //
16
+ // Prefix filter is optional — pass `prefix: "edmund/projects/emdee_os/"`
17
+ // to lint a subtree instead of the whole vault. Useful when a hygiene
18
+ // pass is scoped to one area and you don't want a punch list of every
19
+ // unrelated drift.
20
+
21
+ function json(value: unknown) {
22
+ return { content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }] };
23
+ }
24
+
25
+ export async function lintVault(ctx: ToolContext, args: Record<string, unknown>): Promise<unknown> {
26
+ const prefix = typeof args.prefix === "string" && args.prefix.length > 0 ? args.prefix : null;
27
+ const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : null;
28
+
29
+ const index = await loadVaultIndex(ctx);
30
+
31
+ const perDoc: Array<{ path: string; warnings: LintWarning[] }> = [];
32
+ const byCode: Record<string, number> = {};
33
+ let scanned = 0;
34
+
35
+ for (const doc of index.docs) {
36
+ if (prefix && !doc.path.startsWith(prefix)) continue;
37
+ scanned++;
38
+ const lintCtx = buildLintVaultContext(index, doc.path);
39
+ const result = lintDocContent(doc.content, lintCtx);
40
+ if (result.warnings.length === 0) continue;
41
+ perDoc.push({ path: doc.path, warnings: result.warnings });
42
+ for (const w of result.warnings) {
43
+ byCode[w.code] = (byCode[w.code] ?? 0) + 1;
44
+ }
45
+ }
46
+
47
+ // Sort docs with most warnings first — top of the punch list is where
48
+ // the operator's attention buys the most cleanup per unit of effort.
49
+ perDoc.sort((a, b) => b.warnings.length - a.warnings.length);
50
+ const docs = limit ? perDoc.slice(0, limit) : perDoc;
51
+
52
+ return json({
53
+ scanned,
54
+ with_warnings: perDoc.length,
55
+ warnings_total: Object.values(byCode).reduce((a, b) => a + b, 0),
56
+ warnings_by_code: byCode,
57
+ docs,
58
+ });
59
+ }
@@ -134,18 +134,28 @@ export class SupabaseStorage implements VaultStorage {
134
134
  if (!folder || folder.includes("/")) {
135
135
  return this.walkFolder(folder);
136
136
  }
137
- const { data, error } = await adminClient()
138
- .from(CACHE_TABLE)
139
- .select("file_path, updated_at")
140
- .eq("namespace", folder);
141
- if (!error && data) {
142
- return data.map((r) => ({
143
- path: `${folder}/${r.file_path}`,
144
- content: "",
145
- updatedAt: r.updated_at as string,
146
- }));
137
+ const admin = adminClient();
138
+ // HARD RULE 6: PostgREST caps `.select()` at 1000 rows server-side.
139
+ // Vaults > 1000 docs silently truncate without an explicit range loop.
140
+ const PAGE = 1000;
141
+ const rows: Array<{ file_path: string; updated_at: string }> = [];
142
+ for (let offset = 0; ; offset += PAGE) {
143
+ const { data, error } = await admin
144
+ .from(CACHE_TABLE)
145
+ .select("file_path, updated_at")
146
+ .eq("namespace", folder)
147
+ .order("file_path", { ascending: true })
148
+ .range(offset, offset + PAGE - 1);
149
+ if (error) return this.walkFolder(folder);
150
+ const page = (data ?? []) as Array<{ file_path: string; updated_at: string }>;
151
+ rows.push(...page);
152
+ if (page.length < PAGE) break;
147
153
  }
148
- return this.walkFolder(folder);
154
+ return rows.map((r) => ({
155
+ path: `${folder}/${r.file_path}`,
156
+ content: "",
157
+ updatedAt: r.updated_at,
158
+ }));
149
159
  });
150
160
  }
151
161
 
@@ -186,13 +196,29 @@ export class SupabaseStorage implements VaultStorage {
186
196
  }
187
197
 
188
198
  const admin = adminClient();
189
- const { data, error } = await admin
190
- .from(CACHE_TABLE)
191
- .select("file_path, content, updated_at")
192
- .eq("namespace", folder);
199
+ // HARD RULE 6: PostgREST caps `.select()` at 1000 rows server-side.
200
+ // Vaults > 1000 docs silently truncate without an explicit range loop.
201
+ const PAGE = 1000;
202
+ const rows: Array<{ file_path: string; content: string; updated_at: string }> = [];
203
+ let queryError: unknown = null;
204
+ for (let offset = 0; ; offset += PAGE) {
205
+ const { data, error } = await admin
206
+ .from(CACHE_TABLE)
207
+ .select("file_path, content, updated_at")
208
+ .eq("namespace", folder)
209
+ .order("file_path", { ascending: true })
210
+ .range(offset, offset + PAGE - 1);
211
+ if (error) {
212
+ queryError = error;
213
+ break;
214
+ }
215
+ const page = (data ?? []) as Array<{ file_path: string; content: string; updated_at: string }>;
216
+ rows.push(...page);
217
+ if (page.length < PAGE) break;
218
+ }
193
219
 
194
- if (!error && data && data.length > 0) {
195
- return data.map((r) => ({
220
+ if (!queryError && rows.length > 0) {
221
+ return rows.map((r) => ({
196
222
  path: `${folder}/${r.file_path}`,
197
223
  content: r.content,
198
224
  updatedAt: r.updated_at,