@diegopetrucci/pi-gnosis 0.1.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 (3) hide show
  1. package/README.md +59 -0
  2. package/index.ts +159 -0
  3. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # gnosis
2
+
3
+ A pi extension that exposes [`gnosis`](https://github.com/skorokithakis/gnosis) as an agent tool.
4
+
5
+ `gnosis` is a repo-local knowledge base for durable project context: decisions, rejected alternatives, constraints, operational lessons, and intent that are not obvious from code or docs.
6
+
7
+ ## What it adds
8
+
9
+ - a `gnosis` tool for the agent
10
+ - support for `plan`, `review`, `search`, `latest`, `show`, `topics`, `write`, and `reindex`
11
+ - prompt guidance encouraging the agent to search before implementation and record only durable, non-obvious knowledge
12
+
13
+ This extension intentionally does not add slash commands; it is an agent-facing wrapper around the `gn` CLI. It also intentionally omits `edit` and `rm` actions from the tool surface.
14
+
15
+ ## Requirements
16
+
17
+ Install the `gn` CLI first:
18
+
19
+ ```bash
20
+ brew install --cask skorokithakis/tap/gnosis
21
+ ```
22
+
23
+ Or with Go:
24
+
25
+ ```bash
26
+ go install github.com/skorokithakis/gnosis/cmd/gn@latest
27
+ ```
28
+
29
+ ## Install
30
+
31
+ ### Standalone npm package
32
+
33
+ ```bash
34
+ pi install npm:@diegopetrucci/pi-gnosis
35
+ ```
36
+
37
+ ### Collection package
38
+
39
+ ```bash
40
+ pi install npm:@diegopetrucci/pi-extensions
41
+ ```
42
+
43
+ ### GitHub package
44
+
45
+ ```bash
46
+ pi install git:github.com/diegopetrucci/pi-extensions
47
+ ```
48
+
49
+ Then reload pi:
50
+
51
+ ```text
52
+ /reload
53
+ ```
54
+
55
+ ## Notes
56
+
57
+ - The extension shells out to `gn` in the current Pi working directory.
58
+ - Gnosis stores entries in `.gnosis/entries.jsonl` at the repo root and uses a disposable SQLite FTS5 index for search.
59
+ - `write` mutates the repo-local gnosis knowledge base; `reindex` rebuilds the search cache.
package/index.ts ADDED
@@ -0,0 +1,159 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import {
3
+ DEFAULT_MAX_BYTES,
4
+ DEFAULT_MAX_LINES,
5
+ formatSize,
6
+ truncateTail,
7
+ type ExtensionAPI,
8
+ type ToolExecutionMode,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import { Type, type Static } from "typebox";
11
+
12
+ const GNOSIS_TIMEOUT_MS = 30_000;
13
+
14
+ const GnosisParams = Type.Object({
15
+ action: StringEnum(["plan", "review", "search", "latest", "show", "topics", "write", "reindex"] as const, {
16
+ description: "The gnosis operation to run.",
17
+ }),
18
+ query: Type.Optional(Type.String({ minLength: 1, description: "Search query for action=search. Use uppercase OR/NOT for FTS operators." })),
19
+ target: Type.Optional(Type.String({ minLength: 1, description: "Entry ID prefix or topic for action=show." })),
20
+ topics: Type.Optional(
21
+ Type.Array(Type.String({ minLength: 1 }), {
22
+ minItems: 1,
23
+ description: "Topics for action=write. Each normalized topic must be at least 7 chars.",
24
+ }),
25
+ ),
26
+ text: Type.Optional(Type.String({ minLength: 1, description: "Knowledge text for action=write." })),
27
+ related: Type.Optional(
28
+ Type.Array(Type.String({ minLength: 1 }), { description: "Related entry IDs or unique prefixes for action=write." }),
29
+ ),
30
+ limit: Type.Optional(Type.Integer({ minimum: 1, description: "Positive result limit for action=search or action=latest." })),
31
+ });
32
+
33
+ type GnosisParams = Static<typeof GnosisParams>;
34
+
35
+ function positiveInteger(value: number | undefined, name: string): string | undefined {
36
+ if (value === undefined) return undefined;
37
+ if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer.`);
38
+ return String(value);
39
+ }
40
+
41
+ function requireString(value: string | undefined, name: string): string {
42
+ const trimmed = value?.trim();
43
+ if (!trimmed) throw new Error(`${name} is required.`);
44
+ return trimmed;
45
+ }
46
+
47
+ function buildGnosisArgs(params: GnosisParams): string[] {
48
+ switch (params.action) {
49
+ case "plan":
50
+ return ["help", "plan"];
51
+ case "review":
52
+ return ["help", "review"];
53
+ case "search": {
54
+ const args = ["search", requireString(params.query, "query")];
55
+ const limit = positiveInteger(params.limit, "limit");
56
+ if (limit) args.push("--limit", limit);
57
+ return args;
58
+ }
59
+ case "latest": {
60
+ const args = ["latest"];
61
+ const limit = positiveInteger(params.limit, "limit");
62
+ if (limit) args.push("--limit", limit);
63
+ return args;
64
+ }
65
+ case "show":
66
+ return ["show", requireString(params.target, "target")];
67
+ case "topics":
68
+ return ["topics"];
69
+ case "write": {
70
+ const topics = params.topics?.map((topic) => topic.trim()).filter(Boolean) ?? [];
71
+ if (topics.length === 0) throw new Error("topics is required for action=write.");
72
+ const args = ["write", topics.join(","), requireString(params.text, "text")];
73
+ const related = params.related?.map((id) => id.trim()).filter(Boolean) ?? [];
74
+ if (related.length > 0) args.push("--related", related.join(","));
75
+ return args;
76
+ }
77
+ case "reindex":
78
+ return ["reindex"];
79
+ default: {
80
+ const exhaustive: never = params.action;
81
+ throw new Error(`Unsupported gnosis action: ${exhaustive}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ function installHint(): string {
87
+ return [
88
+ "The `gn` CLI is required for the gnosis extension.",
89
+ "Install it with one of:",
90
+ " brew install --cask skorokithakis/tap/gnosis",
91
+ " go install github.com/skorokithakis/gnosis/cmd/gn@latest",
92
+ ].join("\n");
93
+ }
94
+
95
+ function formatOutput(stdout: string, stderr: string): { text: string; truncated: boolean } {
96
+ const raw = [stdout, stderr].filter(Boolean).join(stderr && stdout ? "\n" : "").trimEnd();
97
+ if (!raw) return { text: "(no output)", truncated: false };
98
+
99
+ const truncation = truncateTail(raw, {
100
+ maxBytes: DEFAULT_MAX_BYTES,
101
+ maxLines: DEFAULT_MAX_LINES,
102
+ });
103
+
104
+ let text = truncation.content;
105
+ if (truncation.truncated) {
106
+ text += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
107
+ text += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).]`;
108
+ }
109
+ return { text, truncated: truncation.truncated };
110
+ }
111
+
112
+ export default function gnosisExtension(pi: ExtensionAPI) {
113
+ pi.registerTool({
114
+ name: "gnosis",
115
+ label: "Gnosis",
116
+ description:
117
+ "Search and record repo-local project knowledge using the `gn` CLI. Supports plan/review doctrine, search, latest, show, topics, write, and reindex. Does not support edit/rm; use shell only with explicit user intent for destructive gnosis maintenance.",
118
+ executionMode: "sequential" as ToolExecutionMode,
119
+ promptSnippet: "Search and record repo-local project knowledge through gnosis (`gn`).",
120
+ promptGuidelines: [
121
+ "Use the gnosis tool before implementing changes that may touch prior architecture, project decisions, rejected alternatives, or human constraints.",
122
+ "Use gnosis with action=\"search\" and uppercase OR between likely topic keywords, for example `auth OR token OR session`.",
123
+ "Use gnosis with action=\"write\" only for durable, non-obvious project knowledge that is not already captured in code, comments, docs, or the commit message.",
124
+ "Prefer code comments over gnosis with action=\"write\" when the knowledge has an obvious specific code anchor.",
125
+ ],
126
+ parameters: GnosisParams,
127
+
128
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
129
+ const args = buildGnosisArgs(params);
130
+
131
+ let result: Awaited<ReturnType<typeof pi.exec>>;
132
+ try {
133
+ result = await pi.exec("gn", args, {
134
+ cwd: ctx.cwd,
135
+ signal,
136
+ timeout: GNOSIS_TIMEOUT_MS,
137
+ });
138
+ } catch (error) {
139
+ const message = error instanceof Error ? error.message : String(error);
140
+ throw new Error(`${installHint()}\n\nExecution error: ${message}`);
141
+ }
142
+
143
+ const output = formatOutput(result.stdout ?? "", result.stderr ?? "");
144
+ if (result.killed) throw new Error(`gnosis ${params.action} timed out or was cancelled.\n\n${output.text}`);
145
+ if (result.code !== 0) {
146
+ throw new Error(`gnosis ${params.action} failed with exit code ${result.code}.\n\n${output.text}\n\n${installHint()}`);
147
+ }
148
+
149
+ return {
150
+ content: [{ type: "text", text: output.text }],
151
+ details: {
152
+ action: params.action,
153
+ args,
154
+ truncated: output.truncated,
155
+ },
156
+ };
157
+ },
158
+ });
159
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@diegopetrucci/pi-gnosis",
3
+ "version": "0.1.0",
4
+ "description": "A pi extension that exposes the gnosis repo-local knowledge base CLI as an agent tool.",
5
+ "keywords": ["pi-package", "pi", "gnosis", "memory", "knowledge-base", "tool"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/diegopetrucci/pi-extensions.git",
10
+ "directory": "extensions/gnosis"
11
+ },
12
+ "files": [
13
+ "index.ts",
14
+ "README.md"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "pi": {
20
+ "extensions": [
21
+ "index.ts"
22
+ ]
23
+ },
24
+ "peerDependencies": {
25
+ "@earendil-works/pi-ai": "*",
26
+ "@earendil-works/pi-coding-agent": "*",
27
+ "typebox": "*"
28
+ }
29
+ }