membot 0.2.0 → 0.2.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.
@@ -110,7 +110,7 @@ Tombstones hide a path from `ls` / `tree` / `search` but `versions` and `read --
110
110
  | ------------------------------------- | ------------------------------------------------------------------------------ |
111
111
  | `membot add <source>` | Ingest file, directory, glob, URL, or `inline:<text>`. Skips unchanged sources; pass `--force` to re-ingest |
112
112
  | `membot ls [prefix]` | List current files (size, mime, refresh status) |
113
- | `membot tree [prefix]` | Render the synthesised logical-path tree |
113
+ | `membot tree [prefix]` | Render the synthesised logical-path tree (`--max-depth`, `--max-items` cap output) |
114
114
  | `membot read <path>` | Read current markdown surrogate (or `--bytes` for original) |
115
115
  | `membot write <path> --content <txt>` | Write inline agent-authored markdown as a new version |
116
116
  | `membot search <query>` | Hybrid search (semantic + BM25); add `--include-history` to search older versions |
@@ -110,7 +110,7 @@ Tombstones hide a path from `ls` / `tree` / `search` but `versions` and `read --
110
110
  | ------------------------------------- | ------------------------------------------------------------------------------ |
111
111
  | `membot add <source>` | Ingest file, directory, glob, URL, or `inline:<text>`. Skips unchanged sources; pass `--force` to re-ingest |
112
112
  | `membot ls [prefix]` | List current files (size, mime, refresh status) |
113
- | `membot tree [prefix]` | Render the synthesised logical-path tree |
113
+ | `membot tree [prefix]` | Render the synthesised logical-path tree (`--max-depth`, `--max-items` cap output) |
114
114
  | `membot read <path>` | Read current markdown surrogate (or `--bytes` for original) |
115
115
  | `membot write <path> --content <txt>` | Write inline agent-authored markdown as a new version |
116
116
  | `membot search <query>` | Hybrid search (semantic + BM25); add `--include-history` to search older versions |
package/README.md CHANGED
@@ -52,7 +52,7 @@ The skill files describe the discover → ingest → search → read → write w
52
52
  | ------------------------------- | --------------------------------------------------------------------------------- |
53
53
  | `membot add <source>` | Ingest a file, directory, glob, URL, or `inline:<text>`. Default `logical_path` mirrors the source (absolute path for local files, `remotes/{host}/{path}` for URLs) so files with the same basename in different projects don't collide. Pass `-p <path>` to override or, on a directory walk, to set a prefix. Skips on unchanged source bytes; pass `--force` to re-ingest. |
54
54
  | `membot ls [prefix]` | List current files (size, mime, refresh status) |
55
- | `membot tree [prefix]` | Render the synthesised logical-path tree |
55
+ | `membot tree [prefix]` | Render the synthesised logical-path tree (`--max-depth`, `--max-items` cap output) |
56
56
  | `membot read <path>` | Read the markdown surrogate (or `--bytes` for original bytes, base64) |
57
57
  | `membot search <query>` | Hybrid search (semantic + BM25); `--include-history` searches older versions |
58
58
  | `membot info <path>` | Inspect metadata (source, fetcher, schedule, digests) without content |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "membot",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Versioned context store with hybrid search for AI agents. Stdio + HTTP MCP server and CLI.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,6 +8,7 @@ interface TreeNode {
8
8
  full_path: string;
9
9
  is_file: boolean;
10
10
  children?: TreeNode[];
11
+ children_truncated?: number;
11
12
  }
12
13
 
13
14
  export const treeOperation = defineOperation({
@@ -18,6 +19,10 @@ export const treeOperation = defineOperation({
18
19
  inputSchema: z.object({
19
20
  prefix: z.string().optional().describe("Only show paths starting with this prefix"),
20
21
  max_depth: z.number().default(4).describe("How many path segments deep to render"),
22
+ max_items: z
23
+ .number()
24
+ .default(20)
25
+ .describe("Max children to render at each level; remainder is summarised as '+N more'"),
21
26
  }),
22
27
  outputSchema: z.object({
23
28
  root: z.string(),
@@ -27,21 +32,30 @@ export const treeOperation = defineOperation({
27
32
  full_path: z.string(),
28
33
  is_file: z.boolean(),
29
34
  children: z.array(z.unknown()).optional(),
35
+ children_truncated: z.number().optional(),
30
36
  }),
31
37
  ),
38
+ truncated: z.number().optional(),
32
39
  }),
33
40
  cli: { positional: ["prefix"] },
34
41
  console_formatter: (result) => {
35
42
  const lines: string[] = [colors.bold(result.root)];
36
43
  const nodes = result.tree as TreeNode[];
37
- renderNodes(nodes, "", lines);
44
+ const topTruncated = (result as { truncated?: number }).truncated ?? 0;
45
+ renderNodes(nodes, "", lines, topTruncated);
38
46
  if (lines.length === 1) lines.push(colors.dim("(empty)"));
39
47
  return lines.join("\n");
40
48
  },
41
49
  handler: async (input, ctx) => {
42
50
  const allPaths = await listAllCurrentPaths(ctx.db);
43
51
  const filtered = input.prefix ? allPaths.filter((p) => p.startsWith(input.prefix!)) : allPaths;
44
- return { root: input.prefix ?? "/", tree: buildTree(filtered, input.max_depth) };
52
+ const tree = buildTree(filtered, input.max_depth);
53
+ const truncated = truncateTree(tree, input.max_items);
54
+ return {
55
+ root: input.prefix ?? "/",
56
+ tree,
57
+ ...(truncated > 0 ? { truncated } : {}),
58
+ };
45
59
  },
46
60
  });
47
61
 
@@ -50,9 +64,10 @@ export const treeOperation = defineOperation({
50
64
  * Splits each path into segments and groups by common prefix. Segments
51
65
  * deeper than `maxDepth` are folded into the deepest visible ancestor —
52
66
  * that ancestor is marked `is_file=true` so the renderer surfaces it as a
53
- * leaf even though longer paths exist underneath.
67
+ * leaf even though longer paths exist underneath. Children are sorted by
68
+ * name within each level so downstream truncation is deterministic.
54
69
  */
55
- function buildTree(paths: string[], maxDepth: number): TreeNode[] {
70
+ export function buildTree(paths: string[], maxDepth: number): TreeNode[] {
56
71
  interface MutableNode {
57
72
  name: string;
58
73
  full_path: string;
@@ -90,19 +105,43 @@ function buildTree(paths: string[], maxDepth: number): TreeNode[] {
90
105
  return finalize(root);
91
106
  }
92
107
 
108
+ /**
109
+ * Trim each child list (and the root list) to `maxItems`, mutating in place.
110
+ * Returns the number of root entries dropped; per-node drops are recorded on
111
+ * `node.children_truncated`. Input is assumed pre-sorted (by `buildTree`) so
112
+ * "first N" is stable.
113
+ */
114
+ export function truncateTree(nodes: TreeNode[], maxItems: number): number {
115
+ for (const node of nodes) {
116
+ if (node.children?.length) {
117
+ const dropped = truncateTree(node.children, maxItems);
118
+ if (dropped > 0) node.children_truncated = dropped;
119
+ }
120
+ }
121
+ if (nodes.length > maxItems) {
122
+ const dropped = nodes.length - maxItems;
123
+ nodes.length = maxItems;
124
+ return dropped;
125
+ }
126
+ return 0;
127
+ }
128
+
93
129
  /**
94
130
  * Walk a tree and append `├── name` / `└── name` lines with proper continuation
95
- * prefixes. Directories are rendered in cyan-bold; files in plain text.
131
+ * prefixes. Directories are rendered in cyan-bold; files in plain text. When a
132
+ * level was truncated, a dim trailing `+N more` line is appended at that level.
96
133
  */
97
- function renderNodes(nodes: TreeNode[], prefix: string, out: string[]): void {
98
- const sorted = [...nodes].sort((a, b) => a.name.localeCompare(b.name));
99
- sorted.forEach((node, i) => {
100
- const last = i === sorted.length - 1;
134
+ function renderNodes(nodes: TreeNode[], prefix: string, out: string[], truncatedCount = 0): void {
135
+ nodes.forEach((node, i) => {
136
+ const last = i === nodes.length - 1 && truncatedCount === 0;
101
137
  const branch = last ? "└── " : "├── ";
102
138
  const label = node.is_file && !node.children?.length ? node.name : colors.cyan(colors.bold(node.name));
103
139
  out.push(`${prefix}${branch}${label}`);
104
140
  if (node.children?.length) {
105
- renderNodes(node.children, prefix + (last ? " " : "│ "), out);
141
+ renderNodes(node.children, prefix + (last ? " " : "│ "), out, node.children_truncated ?? 0);
106
142
  }
107
143
  });
144
+ if (truncatedCount > 0) {
145
+ out.push(`${prefix}└── ${colors.dim(`+${truncatedCount} more`)}`);
146
+ }
108
147
  }