grounder 0.1.0 → 0.2.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 (51) hide show
  1. package/README.md +44 -6
  2. package/dist/agents/claude.d.ts +14 -0
  3. package/dist/agents/claude.d.ts.map +1 -1
  4. package/dist/agents/claude.js +221 -1
  5. package/dist/agents/claude.js.map +1 -1
  6. package/dist/agents/cursor-hook-input.d.ts +14 -0
  7. package/dist/agents/cursor-hook-input.d.ts.map +1 -0
  8. package/dist/agents/cursor-hook-input.js +80 -0
  9. package/dist/agents/cursor-hook-input.js.map +1 -0
  10. package/dist/agents/cursor.d.ts +10 -0
  11. package/dist/agents/cursor.d.ts.map +1 -1
  12. package/dist/agents/cursor.js +171 -1
  13. package/dist/agents/cursor.js.map +1 -1
  14. package/dist/agents/hook-runtime.d.ts +108 -0
  15. package/dist/agents/hook-runtime.d.ts.map +1 -0
  16. package/dist/agents/hook-runtime.js +260 -0
  17. package/dist/agents/hook-runtime.js.map +1 -0
  18. package/dist/agents/types.d.ts +4 -0
  19. package/dist/agents/types.d.ts.map +1 -1
  20. package/dist/cli.js +12 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/commands/doctor.d.ts.map +1 -1
  23. package/dist/commands/doctor.js +68 -2
  24. package/dist/commands/doctor.js.map +1 -1
  25. package/dist/commands/handoff/list.d.ts +9 -0
  26. package/dist/commands/handoff/list.d.ts.map +1 -1
  27. package/dist/commands/handoff/list.js +15 -4
  28. package/dist/commands/handoff/list.js.map +1 -1
  29. package/dist/commands/handoff/peek.d.ts +38 -0
  30. package/dist/commands/handoff/peek.d.ts.map +1 -0
  31. package/dist/commands/handoff/peek.js +120 -0
  32. package/dist/commands/handoff/peek.js.map +1 -0
  33. package/dist/commands/vault/init.d.ts +2 -0
  34. package/dist/commands/vault/init.d.ts.map +1 -1
  35. package/dist/commands/vault/init.js +28 -0
  36. package/dist/commands/vault/init.js.map +1 -1
  37. package/dist/util/frontmatter.d.ts +11 -0
  38. package/dist/util/frontmatter.d.ts.map +1 -0
  39. package/dist/util/frontmatter.js +69 -0
  40. package/dist/util/frontmatter.js.map +1 -0
  41. package/dist/util/merge-json.d.ts +15 -0
  42. package/dist/util/merge-json.d.ts.map +1 -0
  43. package/dist/util/merge-json.js +40 -0
  44. package/dist/util/merge-json.js.map +1 -0
  45. package/dist/vault/find-usable-handoff.d.ts +19 -0
  46. package/dist/vault/find-usable-handoff.d.ts.map +1 -0
  47. package/dist/vault/find-usable-handoff.js +28 -0
  48. package/dist/vault/find-usable-handoff.js.map +1 -0
  49. package/package.json +1 -1
  50. package/templates/agents/claude/commands/grounder-task.md +4 -4
  51. package/templates/agents/cursor/commands/grounder-task.md +4 -4
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Unescape sequences produced by {@link yamlDoubleQuoted}: `\\`, `\"`, `\r`, `\n`.
3
+ */
4
+ function unescapeYamlDoubleQuoted(value) {
5
+ let result = "";
6
+ for (let i = 0; i < value.length; i++) {
7
+ if (value[i] === "\\" && i + 1 < value.length) {
8
+ const next = value[i + 1];
9
+ if (next === "\\" || next === '"') {
10
+ result += next;
11
+ i++;
12
+ continue;
13
+ }
14
+ if (next === "n") {
15
+ result += "\n";
16
+ i++;
17
+ continue;
18
+ }
19
+ if (next === "r") {
20
+ result += "\r";
21
+ i++;
22
+ continue;
23
+ }
24
+ }
25
+ result += value[i];
26
+ }
27
+ return result;
28
+ }
29
+ /**
30
+ * Parse `title` / `created` from Grounder handoff frontmatter.
31
+ * Accepts the quoted `key: "value"` shape {@link writeHandoff} writes today, and
32
+ * unquoted `key: value` from earlier handoffs. Not a general YAML parser.
33
+ * Returns `{}` on anything unexpected; never throws.
34
+ */
35
+ export function parseHandoffFrontmatter(content) {
36
+ try {
37
+ const lines = content.split(/\r?\n/);
38
+ if (lines[0] !== "---") {
39
+ return {};
40
+ }
41
+ const result = {};
42
+ for (let i = 1; i < lines.length; i++) {
43
+ const line = lines[i] ?? "";
44
+ if (line === "---") {
45
+ break;
46
+ }
47
+ const quoted = /^(title|created):\s*"(.*)"\s*$/.exec(line);
48
+ if (quoted) {
49
+ const key = quoted[1];
50
+ result[key] = unescapeYamlDoubleQuoted(quoted[2] ?? "");
51
+ continue;
52
+ }
53
+ // Opening quote without a closing pair — ignore rather than treat as unquoted.
54
+ if (/^(title|created):\s*"/.test(line)) {
55
+ continue;
56
+ }
57
+ const unquoted = /^(title|created):\s*(.+?)\s*$/.exec(line);
58
+ if (unquoted) {
59
+ const key = unquoted[1];
60
+ result[key] = unquoted[2] ?? "";
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ catch {
66
+ return {};
67
+ }
68
+ }
69
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,SAAS,wBAAwB,CAAC,KAAa;IAC7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBAClC,MAAM,IAAI,IAAI,CAAC;gBACf,CAAC,EAAE,CAAC;gBACJ,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,IAAI,IAAI,CAAC;gBACf,CAAC,EAAE,CAAC;gBACJ,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,IAAI,IAAI,CAAC;gBACf,CAAC,EAAE,CAAC;gBACJ,SAAS;YACX,CAAC;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAe;IACrD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;YACvB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,MAAM,GAAyC,EAAE,CAAC;QACxD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;gBACnB,MAAM;YACR,CAAC;YAED,MAAM,MAAM,GAAG,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3D,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAwB,CAAC;gBAC7C,MAAM,CAAC,GAAG,CAAC,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBACxD,SAAS;YACX,CAAC;YAED,+EAA+E;YAC/E,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvC,SAAS;YACX,CAAC;YAED,MAAM,QAAQ,GAAG,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5D,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAwB,CAAC;gBAC/C,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAClC,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ export type MergeJsonResult = {
2
+ ok: true;
3
+ created: boolean;
4
+ } | {
5
+ ok: false;
6
+ error: "unparseable";
7
+ message: string;
8
+ };
9
+ /**
10
+ * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed.
11
+ * On parse failure or non-object root: leaves the file untouched and returns an error
12
+ * so callers can warn without clobbering shared config.
13
+ */
14
+ export declare function mergeJsonFile(filePath: string, merge: (current: Record<string, unknown>) => Record<string, unknown>): Promise<MergeJsonResult>;
15
+ //# sourceMappingURL=merge-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-json.d.ts","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnE,OAAO,CAAC,eAAe,CAAC,CAiC1B"}
@@ -0,0 +1,40 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileExists } from "./fs.js";
4
+ /**
5
+ * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed.
6
+ * On parse failure or non-object root: leaves the file untouched and returns an error
7
+ * so callers can warn without clobbering shared config.
8
+ */
9
+ export async function mergeJsonFile(filePath, merge) {
10
+ const existed = await fileExists(filePath);
11
+ let current = {};
12
+ if (existed) {
13
+ const raw = await readFile(filePath, "utf8");
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(raw);
17
+ }
18
+ catch (err) {
19
+ const detail = err instanceof Error ? err.message : String(err);
20
+ return {
21
+ ok: false,
22
+ error: "unparseable",
23
+ message: `Refusing to modify ${filePath}: invalid JSON (${detail})`,
24
+ };
25
+ }
26
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
27
+ return {
28
+ ok: false,
29
+ error: "unparseable",
30
+ message: `Refusing to modify ${filePath}: root value must be a JSON object`,
31
+ };
32
+ }
33
+ current = parsed;
34
+ }
35
+ const next = merge(current);
36
+ await mkdir(path.dirname(filePath), { recursive: true });
37
+ await writeFile(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
38
+ return { ok: true, created: !existed };
39
+ }
40
+ //# sourceMappingURL=merge-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-json.js","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAMrC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,KAAoE;IAEpE,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,OAAO,GAA4B,EAAE,CAAC;IAE1C,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC7C,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,mBAAmB,MAAM,GAAG;aACpE,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,oCAAoC;aAC5E,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,MAAiC,CAAC;IAC9C,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5B,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC;AACzC,CAAC"}
@@ -0,0 +1,19 @@
1
+ export interface FindUsableHandoffOptions {
2
+ /** Max newest-first candidates to scan (default: 5). */
3
+ limit?: number;
4
+ }
5
+ export interface UsableHandoff {
6
+ /** Absolute path of the resolved handoff file. */
7
+ path: string;
8
+ /** Already-read file contents (avoids a second read by callers). */
9
+ content: string;
10
+ }
11
+ /**
12
+ * Resolves the newest-first handoff under `logsDir` that actually has content,
13
+ * skipping empty or unreadable files along the way (e.g. an interrupted write).
14
+ * Single source of truth for "which handoff is current" — shared by
15
+ * `grounder handoff peek` and `grounder handoff list --head` so both agree.
16
+ * Returns `undefined` when no candidate within the scan window is usable.
17
+ */
18
+ export declare function findUsableHandoff(logsDir: string, options?: FindUsableHandoffOptions): Promise<UsableHandoff | undefined>;
19
+ //# sourceMappingURL=find-usable-handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-usable-handoff.d.ts","sourceRoot":"","sources":["../../src/vault/find-usable-handoff.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,wBAAwB;IACvC,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAcpC"}
@@ -0,0 +1,28 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { listHandoffs } from "./list-handoffs.js";
3
+ /** Newest-first candidates scanned for a usable handoff before giving up. */
4
+ const DEFAULT_SCAN_LIMIT = 5;
5
+ /**
6
+ * Resolves the newest-first handoff under `logsDir` that actually has content,
7
+ * skipping empty or unreadable files along the way (e.g. an interrupted write).
8
+ * Single source of truth for "which handoff is current" — shared by
9
+ * `grounder handoff peek` and `grounder handoff list --head` so both agree.
10
+ * Returns `undefined` when no candidate within the scan window is usable.
11
+ */
12
+ export async function findUsableHandoff(logsDir, options = {}) {
13
+ const paths = await listHandoffs(logsDir, { limit: options.limit ?? DEFAULT_SCAN_LIMIT });
14
+ for (const filePath of paths) {
15
+ let content;
16
+ try {
17
+ content = await readFile(filePath, "utf8");
18
+ }
19
+ catch {
20
+ continue;
21
+ }
22
+ if (content.trim().length > 0) {
23
+ return { path: filePath, content };
24
+ }
25
+ }
26
+ return undefined;
27
+ }
28
+ //# sourceMappingURL=find-usable-handoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-usable-handoff.js","sourceRoot":"","sources":["../../src/vault/find-usable-handoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,6EAA6E;AAC7E,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAc7B;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAe,EACf,UAAoC,EAAE;IAEtC,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,kBAAkB,EAAE,CAAC,CAAC;IAC1F,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC;QAC7B,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;QACrC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grounder",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Connect git projects to Obsidian dev vaults for AI agent memory",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,13 +4,13 @@ Read-only — do not write to the vault. Do not invent vault paths.
4
4
 
5
5
  From the linked project folder or any subdirectory beneath it:
6
6
 
7
- 1. List recent handoffs (newest first):
7
+ 1. Get the current handoff (skips empty/unreadable files, same pick as the session-start teaser):
8
8
 
9
- npx grounder handoff list --limit 5
9
+ npx grounder handoff list --head
10
10
 
11
- 2. If the list is empty: tell the user there are no handoffs yet, then read repo `AGENTS.md` only and proceed.
11
+ 2. If empty: tell the user there are no handoffs yet, then read repo `AGENTS.md` only and proceed.
12
12
 
13
- 3. If handoffs exist: read the newest file (first path). Optionally skim other listed paths if the user names a session or the newest is clearly wrong.
13
+ 3. Otherwise, read that file. If the user names a specific session instead, run `npx grounder handoff list --limit 5` and read the path they mean.
14
14
 
15
15
  4. Read repo `AGENTS.md` (project conventions and constraints).
16
16
 
@@ -4,13 +4,13 @@ Read-only — do not write to the vault. Do not invent vault paths.
4
4
 
5
5
  From the linked project folder or any subdirectory beneath it:
6
6
 
7
- 1. List recent handoffs (newest first):
7
+ 1. Get the current handoff (skips empty/unreadable files, same pick as the session-start teaser):
8
8
 
9
- npx grounder handoff list --limit 5
9
+ npx grounder handoff list --head
10
10
 
11
- 2. If the list is empty: tell the user there are no handoffs yet, then read repo `AGENTS.md` only and proceed.
11
+ 2. If empty: tell the user there are no handoffs yet, then read repo `AGENTS.md` only and proceed.
12
12
 
13
- 3. If handoffs exist: read the newest file (first path). Optionally skim other listed paths if the user names a session or the newest is clearly wrong.
13
+ 3. Otherwise, read that file. If the user names a specific session instead, run `npx grounder handoff list --limit 5` and read the path they mean.
14
14
 
15
15
  4. Read repo `AGENTS.md` (project conventions and constraints).
16
16