@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.
- package/README.md +59 -0
- package/index.ts +159 -0
- 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
|
+
}
|