memonaut 0.1.0 → 0.3.0

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.
Files changed (47) hide show
  1. package/dist/cli-main.d.ts +12 -1
  2. package/dist/cli-main.d.ts.map +1 -1
  3. package/dist/cli-main.js +185 -25
  4. package/dist/cli-main.js.map +1 -1
  5. package/dist/cli.js +1 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/format.d.ts.map +1 -1
  8. package/dist/format.js +13 -0
  9. package/dist/format.js.map +1 -1
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/model.d.ts +23 -1
  15. package/dist/model.d.ts.map +1 -1
  16. package/dist/regex.d.ts +75 -0
  17. package/dist/regex.d.ts.map +1 -0
  18. package/dist/regex.js +242 -0
  19. package/dist/regex.js.map +1 -0
  20. package/dist/resources.d.ts +2 -0
  21. package/dist/resources.d.ts.map +1 -0
  22. package/dist/resources.js +25 -0
  23. package/dist/resources.js.map +1 -0
  24. package/dist/ripgrep.d.ts +52 -0
  25. package/dist/ripgrep.d.ts.map +1 -0
  26. package/dist/ripgrep.js +217 -0
  27. package/dist/ripgrep.js.map +1 -0
  28. package/dist/search.d.ts +22 -1
  29. package/dist/search.d.ts.map +1 -1
  30. package/dist/search.js +47 -23
  31. package/dist/search.js.map +1 -1
  32. package/dist/skills.d.ts +34 -0
  33. package/dist/skills.d.ts.map +1 -0
  34. package/dist/skills.js +59 -0
  35. package/dist/skills.js.map +1 -0
  36. package/package.json +4 -3
  37. package/skills/memonaut/SKILL.md +120 -0
  38. package/src/cli-main.ts +230 -30
  39. package/src/cli.ts +1 -1
  40. package/src/format.ts +12 -0
  41. package/src/index.ts +10 -0
  42. package/src/model.ts +23 -1
  43. package/src/regex.ts +378 -0
  44. package/src/resources.ts +26 -0
  45. package/src/ripgrep.ts +263 -0
  46. package/src/search.ts +68 -26
  47. package/src/skills.ts +86 -0
package/src/search.ts CHANGED
@@ -70,6 +70,40 @@ export function quoteQuery(text: string): string {
70
70
  return quoted.join(' ');
71
71
  }
72
72
 
73
+ /**
74
+ * Which transcripts a query is allowed to see.
75
+ *
76
+ * Visibility is decided per THREAD, because a lineage can in principle span
77
+ * working directories; the lineage set is only a prefilter. Both the FTS and
78
+ * the regex path go through here, so `private` and the cwd/project filters
79
+ * cannot drift apart between them.
80
+ */
81
+ export function visibleSets(
82
+ files: Map<number, FileRecord>,
83
+ query: Pick<SearchQuery, 'cwd' | 'project' | 'includePrivate'>,
84
+ ): {files: Set<number>; lineages: Set<number>; totalLineages: Set<number>} {
85
+ const cwdMatch = query.cwd && query.cwd.length ? matcher(query.cwd) : null;
86
+ const projectMatch =
87
+ query.project && query.project.length ? new Set(query.project) : null;
88
+ const visibleFiles = new Set<number>();
89
+ const visibleLineages = new Set<number>();
90
+ const totalLineages = new Set<number>();
91
+ for (const file of files.values()) {
92
+ if (file.lineage_id !== null) totalLineages.add(Number(file.lineage_id));
93
+ if (!query.includePrivate && file.private) continue;
94
+ if (cwdMatch && !cwdMatch(file.cwd)) continue;
95
+ if (projectMatch && !(file.project && projectMatch.has(file.project)))
96
+ continue;
97
+ visibleFiles.add(Number(file.id));
98
+ if (file.lineage_id !== null) visibleLineages.add(Number(file.lineage_id));
99
+ }
100
+ return {
101
+ files: visibleFiles,
102
+ lineages: visibleLineages,
103
+ totalLineages,
104
+ };
105
+ }
106
+
73
107
  function inList(
74
108
  column: string,
75
109
  values: string[] | undefined,
@@ -109,25 +143,11 @@ export function search(
109
143
  const threadLimit = query.threadLimit ?? 3;
110
144
  const files = loadFiles(db);
111
145
 
112
- // Visibility is decided per THREAD, because a lineage can in principle span
113
- // working directories. The lineage set below is only a prefilter.
114
- const cwdMatch = query.cwd && query.cwd.length ? matcher(query.cwd) : null;
115
- const projectMatch =
116
- query.project && query.project.length ? new Set(query.project) : null;
117
- const visibleFiles = new Set<number>();
118
- const visibleLineages = new Set<number>();
119
- for (const file of files.values()) {
120
- if (!query.includePrivate && file.private) continue;
121
- if (cwdMatch && !cwdMatch(file.cwd)) continue;
122
- if (projectMatch && !(file.project && projectMatch.has(file.project)))
123
- continue;
124
- visibleFiles.add(Number(file.id));
125
- if (file.lineage_id !== null) visibleLineages.add(Number(file.lineage_id));
126
- }
127
-
128
- const totalLineages = new Set<number>();
129
- for (const file of files.values())
130
- if (file.lineage_id !== null) totalLineages.add(Number(file.lineage_id));
146
+ const {
147
+ files: visibleFiles,
148
+ lineages: visibleLineages,
149
+ totalLineages,
150
+ } = visibleSets(files, query);
131
151
 
132
152
  const clauses: string[] = [];
133
153
  const args: unknown[] = [];
@@ -259,16 +279,20 @@ export function search(
259
279
  };
260
280
  }
261
281
 
262
- function buildHit(
263
- row: RawHit,
264
- kind: ChunkKind,
265
- score: number,
282
+ /**
283
+ * Fan one entry back out to every visible thread that carries it.
284
+ *
285
+ * Shared by the FTS and the regex path, because they must produce identical
286
+ * thread lists: an entry is stored once and `membership` is what turns it back
287
+ * into "the twelve threads that inherited this".
288
+ */
289
+ export function collectThreads(
290
+ entryId: number,
266
291
  memberStmt: ReturnType<DB['prepare']>,
267
292
  files: Map<number, FileRecord>,
268
293
  visibleFiles: Set<number>,
269
- threadLimit: number,
270
- ): SearchHit | null {
271
- const members = memberStmt.all(row.entry_id) as unknown as Array<{
294
+ ): ThreadRef[] {
295
+ const members = memberStmt.all(entryId) as unknown as Array<{
272
296
  file_id: number;
273
297
  seq: number;
274
298
  }>;
@@ -297,6 +321,24 @@ function buildHit(
297
321
  threads.sort((a, b) =>
298
322
  (b.lastActivity ?? '').localeCompare(a.lastActivity ?? ''),
299
323
  );
324
+ return threads;
325
+ }
326
+
327
+ function buildHit(
328
+ row: RawHit,
329
+ kind: ChunkKind,
330
+ score: number,
331
+ memberStmt: ReturnType<DB['prepare']>,
332
+ files: Map<number, FileRecord>,
333
+ visibleFiles: Set<number>,
334
+ threadLimit: number,
335
+ ): SearchHit | null {
336
+ const threads = collectThreads(
337
+ Number(row.entry_id),
338
+ memberStmt,
339
+ files,
340
+ visibleFiles,
341
+ );
300
342
  if (threads.length === 0) return null;
301
343
 
302
344
  return {
package/src/skills.ts ADDED
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import {resolvePackageResource} from './resources.js';
5
+
6
+ /**
7
+ * Installing the agent skill that ships with this package.
8
+ *
9
+ * pi loads the skill itself from `memonaut-pi` (its `pi.skills` entry), so this command exists for
10
+ * every other agent: the skill is the same file, and `~/.agents/skills/<name>` is where agents that
11
+ * are not pi look for it. One directory, no fan-out to twenty agent-specific locations, and the
12
+ * report names every path written.
13
+ */
14
+
15
+ export interface Skill {
16
+ name: string;
17
+ description: string;
18
+ from: string;
19
+ }
20
+
21
+ export type Scope = 'user' | 'project';
22
+
23
+ /** Where skills live, by convention. `project` keeps them beside the code that needs them. */
24
+ export function destinationFor(scope: Scope, cwd = process.cwd()): string {
25
+ return scope === 'user'
26
+ ? path.join(os.homedir(), '.agents', 'skills')
27
+ : path.join(cwd, '.agents', 'skills');
28
+ }
29
+
30
+ export function skillsSource(): string | undefined {
31
+ return resolvePackageResource('skills/');
32
+ }
33
+
34
+ /** Name and description come from the SKILL.md front matter, so there is one source of truth. */
35
+ export function availableSkills(): Skill[] {
36
+ const root = skillsSource();
37
+ if (!root) return [];
38
+ const skills: Skill[] = [];
39
+ for (const entry of fs.readdirSync(root, {withFileTypes: true})) {
40
+ if (!entry.isDirectory()) continue;
41
+ const manifest = path.join(root, entry.name, 'SKILL.md');
42
+ if (!fs.existsSync(manifest)) continue;
43
+ const text = fs.readFileSync(manifest, 'utf8');
44
+ skills.push({
45
+ name: /^name:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? entry.name,
46
+ description: /^description:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? '',
47
+ from: path.join(root, entry.name),
48
+ });
49
+ }
50
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
51
+ }
52
+
53
+ export interface Installed {
54
+ name: string;
55
+ to: string;
56
+ replaced: boolean;
57
+ }
58
+
59
+ /**
60
+ * Copies rather than symlinks, so an installed skill survives `node_modules` being deleted.
61
+ *
62
+ * The cost is that upgrading the package does not upgrade the installed skill, which is why the
63
+ * report says `replaced` versus `installed`: a skill that is quietly stale is worse than one you
64
+ * know you have to refresh.
65
+ */
66
+ export function installSkills(scope: Scope, cwd = process.cwd()): Installed[] {
67
+ const destination = destinationFor(scope, cwd);
68
+ const installed: Installed[] = [];
69
+ for (const skill of availableSkills()) {
70
+ const to = path.join(destination, skill.name);
71
+ const replaced = fs.existsSync(to);
72
+ if (replaced) fs.rmSync(to, {recursive: true, force: true});
73
+ fs.mkdirSync(destination, {recursive: true});
74
+ fs.cpSync(skill.from, to, {recursive: true});
75
+ installed.push({name: skill.name, to, replaced});
76
+ }
77
+ return installed;
78
+ }
79
+
80
+ export function isInstalled(
81
+ skill: Skill,
82
+ scope: Scope,
83
+ cwd = process.cwd(),
84
+ ): boolean {
85
+ return fs.existsSync(path.join(destinationFor(scope, cwd), skill.name));
86
+ }