@tpsdev-ai/pi-flair 0.8.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 +141 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +314 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# @tpsdev-ai/pi-flair
|
|
2
|
+
|
|
3
|
+
Pi extension for Flair memory access — persistent memory from within pi sessions.
|
|
4
|
+
|
|
5
|
+
## Design Decision
|
|
6
|
+
|
|
7
|
+
**Implementation Path: Native pi Extension (Option B)**
|
|
8
|
+
|
|
9
|
+
- **MCP clients are NOT first-class in pi** — pi's core has no MCP client support. MCP appears only as anthropic-specific beta features in the SDK (`BetaMCPToolUseBlock`, etc.), not as a generic extension mechanism.
|
|
10
|
+
- **Option A (wrap flair-mcp)** would require:
|
|
11
|
+
- Waiting for pi to support MCP servers natively
|
|
12
|
+
- Deprecating flair-mcp's stdio transport in favor of HTTP-only
|
|
13
|
+
- Splitting maintenance between MCP and pi extensions
|
|
14
|
+
- **Option B (native extension)** wins because:
|
|
15
|
+
- Direct HTTP calls via `@tpsdev-ai/flair-client` (zero extra dependencies)
|
|
16
|
+
- Full control over tool registration and session lifecycle hooks
|
|
17
|
+
- Parity with flair-mcp features (search, store, bootstrap)
|
|
18
|
+
- Works today — no pi roadmap dependency
|
|
19
|
+
|
|
20
|
+
Reference: [pi extensions docs](https://pi.dev/docs/extensions.md)
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Prerequisites
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @tpsdev-ai/flair
|
|
28
|
+
flair init
|
|
29
|
+
flair agent add my-agent
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pi install npm:@tpsdev-ai/pi-flair
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or project-local:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pi install -l npm:@tpsdev-ai/pi-flair
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Configure
|
|
45
|
+
|
|
46
|
+
Add to `~/.pi/agent/settings.json` or `.pi/settings.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"extensions": ["npm:@tpsdev-ai/pi-flair"],
|
|
51
|
+
"packages": ["npm:@tpsdev-ai/pi-flair"]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or use environment variables:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export FLAIR_AGENT_ID=my-agent
|
|
59
|
+
export FLAIR_URL=http://127.0.0.1:9926
|
|
60
|
+
pi
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
| Tool | Description |
|
|
66
|
+
|------|-------------|
|
|
67
|
+
| `memory_search` | Search memories by meaning. Understands temporal queries. |
|
|
68
|
+
| `memory_store` | Save memories with type + durability (permanent/persistent/standard/ephemeral). |
|
|
69
|
+
| `bootstrap` | Load session context: soul + memories + predicted context. |
|
|
70
|
+
|
|
71
|
+
## Configuration Options
|
|
72
|
+
|
|
73
|
+
| Variable | Default | Description |
|
|
74
|
+
|----------|---------|-------------|
|
|
75
|
+
| `FLAIR_AGENT_ID` | *(required)* | Agent identity for memory scoping |
|
|
76
|
+
| `FLAIR_URL` | `http://127.0.0.1:9926` | Flair server URL |
|
|
77
|
+
| `FLAIR_KEY_PATH` | auto-resolved | Path to Ed25519 private key |
|
|
78
|
+
| `FLAIR_MAX_RECALL_RESULTS` | `5` | Max results for memory_search |
|
|
79
|
+
| `FLAIR_MAX_BOOTSTRAP_TOKENS` | `4000` | Max tokens in bootstrap output |
|
|
80
|
+
| `FLAIR_AUTO_RECALL` | `false` | Auto-load bootstrap on session start (opt-in) |
|
|
81
|
+
| `FLAIR_AUTO_CAPTURE` | `false` | Auto-save session context to memory |
|
|
82
|
+
|
|
83
|
+
## Security Notes
|
|
84
|
+
|
|
85
|
+
### Auto-Capture Warning
|
|
86
|
+
|
|
87
|
+
When `FLAIR_AUTO_CAPTURE=true`, all assistant responses are persisted to Flair memory with **ephemeral durability**. **This includes any secrets, credentials, or tokens your LLM may output.**
|
|
88
|
+
|
|
89
|
+
**Do not enable `FLAIR_AUTO_CAPTURE=true` if your sessions may output:**
|
|
90
|
+
|
|
91
|
+
- API keys (`sk-`, `ghp_`, `pat_`, etc.)
|
|
92
|
+
- Bearer tokens (`Bearer ` prefix)
|
|
93
|
+
- Private keys (`-----BEGIN PRIVATE KEY-----`, `-----BEGIN RSA PRIVATE KEY-----`)
|
|
94
|
+
- AWS/GCP/Azure credentials
|
|
95
|
+
- Any other sensitive data
|
|
96
|
+
|
|
97
|
+
Auto-capture is best-effort and uses `dedup: false` to ensure all content is captured. For production use, disable auto-capture and store only non-sensitive summaries manually via `memory_store`.
|
|
98
|
+
|
|
99
|
+
## How It Works
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
pi (extension) ↔ HTTP ↔ Flair (Harper)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The extension calls Flair's HTTP API directly via `@tpsdev-ai/flair-client`. All memory is stored locally in `~/.flair/` with Ed25519 authentication.
|
|
106
|
+
|
|
107
|
+
## Examples
|
|
108
|
+
|
|
109
|
+
### Semantic Search
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// In a pi session:
|
|
113
|
+
memory_search(query: "what did I decide about auth flow?", limit: 5)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Store Memory
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
memory_store(
|
|
120
|
+
content: "PR reviews must include security assessment",
|
|
121
|
+
durability: "persistent"
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Bootstrap
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
bootstrap(maxTokens: 4000)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Testing
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
cd packages/pi-flair
|
|
135
|
+
npm run build
|
|
136
|
+
# Run tests (TBD)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
[Apache 2.0](../../LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Extension for Flair Memory Access
|
|
3
|
+
*
|
|
4
|
+
* Adds Flair memory tools to pi sessions:
|
|
5
|
+
* - memory_search(agentId, query, limit) — semantic search
|
|
6
|
+
* - memory_store(agentId, content, durability) — save memories
|
|
7
|
+
* - bootstrap(agentId, maxTokens) — cold-start context
|
|
8
|
+
*
|
|
9
|
+
* Configuration:
|
|
10
|
+
* - flair_url (default: http://127.0.0.1:9926)
|
|
11
|
+
* - agentId (required, via FLAIR_AGENT_ID env var)
|
|
12
|
+
* - max_recall_results (default: 5)
|
|
13
|
+
* - max_bootstrap_tokens (default: 4000)
|
|
14
|
+
* - auto_capture (default: false) — auto-save session context to memory
|
|
15
|
+
* - auto_recall (default: false) — auto-load bootstrap on session start
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* 1. Install: pi install npm:@tpsdev-ai/pi-flair
|
|
19
|
+
* 2. Configure in ~/.pi/agent/settings.json or .pi/settings.json:
|
|
20
|
+
* {
|
|
21
|
+
* "extensions": ["npm:@tpsdev-ai/pi-flair"],
|
|
22
|
+
* "flair_url": "http://127.0.0.1:9926",
|
|
23
|
+
* "agentId": "my-project"
|
|
24
|
+
* }
|
|
25
|
+
* 3. Or use environment variables:
|
|
26
|
+
* export FLAIR_AGENT_ID=my-agent
|
|
27
|
+
* export FLAIR_URL=http://127.0.0.1:9926
|
|
28
|
+
* 4. Restart pi
|
|
29
|
+
*/
|
|
30
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
31
|
+
import { type FlairClientConfig } from "@tpsdev-ai/flair-client";
|
|
32
|
+
interface PluginConfig extends FlairClientConfig {
|
|
33
|
+
max_recall_results?: number;
|
|
34
|
+
max_bootstrap_tokens?: number;
|
|
35
|
+
auto_capture?: boolean;
|
|
36
|
+
auto_recall?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Classify an error into a user-friendly message.
|
|
40
|
+
* @param err - The error to classify
|
|
41
|
+
* @param flairUrl - The Flair server URL for connection error messages
|
|
42
|
+
*/
|
|
43
|
+
export declare function classifyError(err: unknown, flairUrl: string): string;
|
|
44
|
+
export default function (pi: ExtensionAPI): void;
|
|
45
|
+
export declare function manualBootstrap(config: PluginConfig, maxTokens?: number): Promise<string>;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,+BAA+B,CAAC;AAEpF,OAAO,EAA2B,KAAK,iBAAiB,EAAwB,MAAM,yBAAyB,CAAC;AAIhH,UAAU,YAAa,SAAQ,iBAAiB;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAkFD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoBpE;AAQD,MAAM,CAAC,OAAO,WAAW,EAAE,EAAE,YAAY,QAyMxC;AAID,wBAAsB,eAAe,CACnC,MAAM,EAAE,YAAY,EACpB,SAAS,SAAO,GACf,OAAO,CAAC,MAAM,CAAC,CAIjB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Extension for Flair Memory Access
|
|
3
|
+
*
|
|
4
|
+
* Adds Flair memory tools to pi sessions:
|
|
5
|
+
* - memory_search(agentId, query, limit) — semantic search
|
|
6
|
+
* - memory_store(agentId, content, durability) — save memories
|
|
7
|
+
* - bootstrap(agentId, maxTokens) — cold-start context
|
|
8
|
+
*
|
|
9
|
+
* Configuration:
|
|
10
|
+
* - flair_url (default: http://127.0.0.1:9926)
|
|
11
|
+
* - agentId (required, via FLAIR_AGENT_ID env var)
|
|
12
|
+
* - max_recall_results (default: 5)
|
|
13
|
+
* - max_bootstrap_tokens (default: 4000)
|
|
14
|
+
* - auto_capture (default: false) — auto-save session context to memory
|
|
15
|
+
* - auto_recall (default: false) — auto-load bootstrap on session start
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* 1. Install: pi install npm:@tpsdev-ai/pi-flair
|
|
19
|
+
* 2. Configure in ~/.pi/agent/settings.json or .pi/settings.json:
|
|
20
|
+
* {
|
|
21
|
+
* "extensions": ["npm:@tpsdev-ai/pi-flair"],
|
|
22
|
+
* "flair_url": "http://127.0.0.1:9926",
|
|
23
|
+
* "agentId": "my-project"
|
|
24
|
+
* }
|
|
25
|
+
* 3. Or use environment variables:
|
|
26
|
+
* export FLAIR_AGENT_ID=my-agent
|
|
27
|
+
* export FLAIR_URL=http://127.0.0.1:9926
|
|
28
|
+
* 4. Restart pi
|
|
29
|
+
*/
|
|
30
|
+
import { Type } from "@sinclair/typebox";
|
|
31
|
+
import { FlairClient, FlairError } from "@tpsdev-ai/flair-client";
|
|
32
|
+
// ─── Secret Filtering for Auto-Capture ───────────────────────────────────────
|
|
33
|
+
// Secret patterns to filter from auto-capture
|
|
34
|
+
const SECRET_PATTERNS = [
|
|
35
|
+
/sk-[a-zA-Z0-9]+/u, // OpenAI keys
|
|
36
|
+
/ghp_[a-zA-Z0-9_.-]+/u, // GitHub PATs (includes dots for JWT-like tokens)
|
|
37
|
+
/pat_[a-zA-Z0-9_.-]+/u, // Generic PATs (includes dots for JWT-like tokens)
|
|
38
|
+
/Bearer [a-zA-Z0-9_.-]+/u, // Bearer tokens (includes dots for JWTs)
|
|
39
|
+
/-----BEGIN PRIVATE KEY-----/u, // Private keys
|
|
40
|
+
/-----BEGIN RSA PRIVATE KEY-----/u, // RSA keys
|
|
41
|
+
/-----BEGIN EC PRIVATE KEY-----/u, // EC keys
|
|
42
|
+
];
|
|
43
|
+
function containsSecrets(text) {
|
|
44
|
+
return SECRET_PATTERNS.some((pattern) => pattern.test(text));
|
|
45
|
+
}
|
|
46
|
+
// ─── Config Resolution ───────────────────────────────────────────────────────
|
|
47
|
+
function getConfig(pi) {
|
|
48
|
+
// Try to load from settings
|
|
49
|
+
// Note: pi doesn't expose settings.json directly in extension API
|
|
50
|
+
// We rely on environment variables and defaults
|
|
51
|
+
const flConfig = {
|
|
52
|
+
url: process.env.FLAIR_URL || "http://127.0.0.1:9926",
|
|
53
|
+
agentId: process.env.FLAIR_AGENT_ID,
|
|
54
|
+
keyPath: process.env.FLAIR_KEY_PATH,
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
...flConfig,
|
|
58
|
+
max_recall_results: parseInt(process.env.FLAIR_MAX_RECALL_RESULTS || "5", 10),
|
|
59
|
+
max_bootstrap_tokens: parseInt(process.env.FLAIR_MAX_BOOTSTRAP_TOKENS || "4000", 10),
|
|
60
|
+
auto_capture: process.env.FLAIR_AUTO_CAPTURE === "true",
|
|
61
|
+
auto_recall: process.env.FLAIR_AUTO_RECALL === "true", // default false (user must explicitly opt-in)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function getAgentId(config, ctx) {
|
|
65
|
+
if (config.agentId)
|
|
66
|
+
return config.agentId;
|
|
67
|
+
// Try to infer from working directory
|
|
68
|
+
const cwd = ctx.cwd;
|
|
69
|
+
const lastSlash = cwd.lastIndexOf("/");
|
|
70
|
+
let agentId;
|
|
71
|
+
if (lastSlash >= 0) {
|
|
72
|
+
agentId = cwd.slice(lastSlash + 1);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
agentId = cwd;
|
|
76
|
+
}
|
|
77
|
+
console.warn(`Flair: agentId not configured, falling back to cwd segment "${agentId}". Set FLAIR_AGENT_ID to silence this warning.`);
|
|
78
|
+
return agentId;
|
|
79
|
+
}
|
|
80
|
+
function createFlairClient(config) {
|
|
81
|
+
if (!config.agentId) {
|
|
82
|
+
throw new Error("FLAIR_AGENT_ID is required");
|
|
83
|
+
}
|
|
84
|
+
return new FlairClient({
|
|
85
|
+
agentId: config.agentId,
|
|
86
|
+
url: config.url || "http://127.0.0.1:9926",
|
|
87
|
+
keyPath: config.keyPath,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// ─── Error Classification ─────────────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* Classify an error into a user-friendly message.
|
|
93
|
+
* @param err - The error to classify
|
|
94
|
+
* @param flairUrl - The Flair server URL for connection error messages
|
|
95
|
+
*/
|
|
96
|
+
export function classifyError(err, flairUrl) {
|
|
97
|
+
if (err instanceof FlairError) {
|
|
98
|
+
const { status, body } = err;
|
|
99
|
+
if (status === 400)
|
|
100
|
+
return `validation_error: ${body}`;
|
|
101
|
+
if (status === 401 || status === 403)
|
|
102
|
+
return `auth_error: ${body}`;
|
|
103
|
+
if (status === 413)
|
|
104
|
+
return `payload_too_large: ${body}`;
|
|
105
|
+
if (status === 429)
|
|
106
|
+
return "rate_limited — retry after a moment";
|
|
107
|
+
if (status >= 500)
|
|
108
|
+
return `server_error (retriable): ${body}`;
|
|
109
|
+
return `http_error (${status}): ${body}`;
|
|
110
|
+
}
|
|
111
|
+
if (err instanceof Error) {
|
|
112
|
+
if (err.name.includes("Abort") || err.name.includes("Timeout")) {
|
|
113
|
+
return "timeout — the server took too long. Try shorter content or retry.";
|
|
114
|
+
}
|
|
115
|
+
if (err instanceof TypeError && err.message.includes("fetch")) {
|
|
116
|
+
return `connection_error (retriable): could not reach Flair at ${flairUrl}. Is it running?`;
|
|
117
|
+
}
|
|
118
|
+
return `unexpected_error: ${err.message}`;
|
|
119
|
+
}
|
|
120
|
+
return `unexpected_error: ${String(err)}`;
|
|
121
|
+
}
|
|
122
|
+
function errorResult(err, flairUrl) {
|
|
123
|
+
return { content: [{ type: "text", text: classifyError(err, flairUrl) }], details: {}, isError: true };
|
|
124
|
+
}
|
|
125
|
+
// ─── Extension Entry Point ────────────────────────────────────────────────────
|
|
126
|
+
export default function (pi) {
|
|
127
|
+
const config = getConfig(pi);
|
|
128
|
+
const flair = createFlairClient(config);
|
|
129
|
+
// ─── Tools ───────────────────────────────────────────────────────────────────
|
|
130
|
+
// memory_search tool
|
|
131
|
+
pi.registerTool({
|
|
132
|
+
name: "memory_search",
|
|
133
|
+
label: "Search Flair Memories",
|
|
134
|
+
description: "Search memories by meaning. Understands temporal queries like 'what happened today'.",
|
|
135
|
+
promptSnippet: "Search Flair memories for relevant context.",
|
|
136
|
+
promptGuidelines: [
|
|
137
|
+
"Use memory_search when you need to recall past conversations, decisions, or lessons from this project.",
|
|
138
|
+
"Use memory_search when the user asks about 'today', 'recently', or 'previously' in this session.",
|
|
139
|
+
],
|
|
140
|
+
parameters: Type.Object({
|
|
141
|
+
query: Type.String({ description: "Search query — natural language, semantic matching" }),
|
|
142
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 5)" })),
|
|
143
|
+
}),
|
|
144
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
145
|
+
try {
|
|
146
|
+
const query = params.query;
|
|
147
|
+
const limit = params.limit ?? config.max_recall_results;
|
|
148
|
+
const results = await flair.memory.search(query, { limit });
|
|
149
|
+
if (results.length === 0) {
|
|
150
|
+
return { content: [{ type: "text", text: "No relevant memories found." }], details: {} };
|
|
151
|
+
}
|
|
152
|
+
const text = results
|
|
153
|
+
.map((r, i) => {
|
|
154
|
+
const date = r.createdAt ? r.createdAt.slice(0, 10) : "";
|
|
155
|
+
const idStr = r.id ? `id:${r.id}` : "";
|
|
156
|
+
const meta = [date, r.type, idStr].filter(Boolean).join(", ");
|
|
157
|
+
return `${i + 1}. ${r.content}${meta ? ` (${meta})` : ""}`;
|
|
158
|
+
})
|
|
159
|
+
.join("\n");
|
|
160
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
return errorResult(err, flair.url);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
// memory_store tool
|
|
168
|
+
pi.registerTool({
|
|
169
|
+
name: "memory_store",
|
|
170
|
+
label: "Store Memory in Flair",
|
|
171
|
+
description: "Save information to persistent memory. Use for lessons, decisions, preferences, facts.",
|
|
172
|
+
promptSnippet: "Store memories for future recall.",
|
|
173
|
+
promptGuidelines: [
|
|
174
|
+
"Use memory_store to record important decisions, lessons learned, or preferences.",
|
|
175
|
+
"Use memory_store before finishing a session to capture key insights.",
|
|
176
|
+
],
|
|
177
|
+
parameters: Type.Object({
|
|
178
|
+
content: Type.String({ description: "What to remember" }),
|
|
179
|
+
durability: Type.Optional(Type.String({
|
|
180
|
+
enum: ["permanent", "persistent", "standard", "ephemeral"],
|
|
181
|
+
description: "permanent — inviolable facts, identity, explicit never-forget (e.g., 'my name is Nathan')\n" +
|
|
182
|
+
"persistent — key decisions and lessons to recall weeks later (e.g., 'PR review process')\n" +
|
|
183
|
+
"standard — default working memory, recent context (e.g., 'discussed auth flow today')\n" +
|
|
184
|
+
"ephemeral — scratch state, auto-expires 72h (e.g., 'currently debugging issue #42')",
|
|
185
|
+
})),
|
|
186
|
+
tags: Type.Optional(Type.Array(Type.String(), { description: "Array of tag strings" })),
|
|
187
|
+
}),
|
|
188
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
189
|
+
try {
|
|
190
|
+
const content = params.content;
|
|
191
|
+
const durability = params.durability || "standard";
|
|
192
|
+
const result = await flair.memory.write(content, {
|
|
193
|
+
type: "session",
|
|
194
|
+
durability: durability,
|
|
195
|
+
tags: params.tags,
|
|
196
|
+
dedup: true,
|
|
197
|
+
dedupThreshold: 0.95,
|
|
198
|
+
});
|
|
199
|
+
// Check if dedup returned an existing memory
|
|
200
|
+
const agentId = getAgentId(config, ctx);
|
|
201
|
+
const generatedPrefix = `${agentId}-`;
|
|
202
|
+
const wasDeduped = result.id && !result.id.startsWith(generatedPrefix);
|
|
203
|
+
if (wasDeduped) {
|
|
204
|
+
return { content: [{ type: "text", text: `Similar memory already exists (id: ${result.id}): ${result.content?.slice(0, 200)}` }], details: {} };
|
|
205
|
+
}
|
|
206
|
+
const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
|
|
207
|
+
const tagStr = params.tags?.length ? params.tags.join(", ") : "none";
|
|
208
|
+
const text = [
|
|
209
|
+
`Memory stored (id: ${result.id})`,
|
|
210
|
+
`Preview: ${preview}`,
|
|
211
|
+
`Size: ${content.length} chars`,
|
|
212
|
+
`Tags: ${tagStr}`,
|
|
213
|
+
`Durability: ${durability}`,
|
|
214
|
+
].join("\n");
|
|
215
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
return errorResult(err, flair.url);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
// bootstrap tool
|
|
223
|
+
pi.registerTool({
|
|
224
|
+
name: "bootstrap",
|
|
225
|
+
label: "Bootstrap Flair Context",
|
|
226
|
+
description: "Get session context: soul + memories + predicted context. Run at session start.",
|
|
227
|
+
promptSnippet: "Load session context from Flair.",
|
|
228
|
+
promptGuidelines: [
|
|
229
|
+
"Call bootstrap at the start of a new session to load relevant past memories.",
|
|
230
|
+
"Only call bootstrap once per session — it's expensive.",
|
|
231
|
+
],
|
|
232
|
+
parameters: Type.Object({
|
|
233
|
+
maxTokens: Type.Optional(Type.Number({ description: "Max tokens in output (default 4000)" })),
|
|
234
|
+
}),
|
|
235
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
236
|
+
try {
|
|
237
|
+
const maxTokens = params.maxTokens ?? config.max_bootstrap_tokens;
|
|
238
|
+
const result = await flair.bootstrap({ maxTokens });
|
|
239
|
+
if (!result.context) {
|
|
240
|
+
return { content: [{ type: "text", text: "No context available." }], details: {} };
|
|
241
|
+
}
|
|
242
|
+
return { content: [{ type: "text", text: result.context }], details: {} };
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
return errorResult(err, flair.url);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
// ─── Auto-Recall on Session Start ────────────────────────────────────────────
|
|
250
|
+
if (config.auto_recall !== false) {
|
|
251
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
252
|
+
// In auto_recall mode, inject bootstrap context via before_agent_start
|
|
253
|
+
// This gives the LLM relevant memories at session start
|
|
254
|
+
ctx.ui.notify("Flair: loading session context...", "info");
|
|
255
|
+
try {
|
|
256
|
+
const maxTokens = config.max_bootstrap_tokens ?? 4000;
|
|
257
|
+
const result = await flair.bootstrap({ maxTokens });
|
|
258
|
+
if (result.context) {
|
|
259
|
+
// Inject a system message with the context
|
|
260
|
+
pi.appendEntry("flair-bootstrap", {
|
|
261
|
+
context: result.context,
|
|
262
|
+
timestamp: Date.now(),
|
|
263
|
+
});
|
|
264
|
+
ctx.ui.notify("Flair: context loaded", "info");
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
ctx.ui.notify("Flair: no context available", "info");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
ctx.ui.notify(`Flair bootstrap failed: ${classifyError(err, flair.url)}`, "error");
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// ─── Auto-Capture on Turn End (optional) ────────────────────────────────────
|
|
276
|
+
if (config.auto_capture === true) {
|
|
277
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
278
|
+
// Extract key insights from the turn and store them
|
|
279
|
+
// This is a simplified version — real implementation would parse the conversation
|
|
280
|
+
const branch = ctx.sessionManager.getBranch();
|
|
281
|
+
if (branch.length >= 2) {
|
|
282
|
+
const lastEntry = branch[branch.length - 2];
|
|
283
|
+
if (lastEntry.type === "message" && lastEntry.message.role === "assistant" && lastEntry.message.content) {
|
|
284
|
+
// Convert to string for storage
|
|
285
|
+
const content = JSON.stringify(lastEntry.message.content);
|
|
286
|
+
// Filter out content containing secrets
|
|
287
|
+
if (containsSecrets(content)) {
|
|
288
|
+
console.warn("Auto-capture skipped: potential secrets detected");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (content.length > 100) {
|
|
292
|
+
try {
|
|
293
|
+
await flair.memory.write(content.slice(0, 4000), {
|
|
294
|
+
type: "session",
|
|
295
|
+
durability: "ephemeral",
|
|
296
|
+
dedup: false,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
// Silently fail — auto-capture is best-effort
|
|
301
|
+
console.warn("Auto-capture failed:", classifyError(err, flair.url));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// ─── Helper for manual bootstrap injection ────────────────────────────────────
|
|
310
|
+
export async function manualBootstrap(config, maxTokens = 4000) {
|
|
311
|
+
const flair = createFlairClient(config);
|
|
312
|
+
const result = await flair.bootstrap({ maxTokens });
|
|
313
|
+
return result.context || "No context available.";
|
|
314
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tpsdev-ai/pi-flair",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Flair memory extension for pi — persistent memory access from within pi sessions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@tpsdev-ai/flair-client": "0.8.0",
|
|
25
|
+
"@sinclair/typebox": "0.34.48"
|
|
26
|
+
},
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/tpsdev-ai/flair.git",
|
|
31
|
+
"directory": "packages/pi-flair"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://tps.dev/#flair",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"pi",
|
|
36
|
+
"pi-extension",
|
|
37
|
+
"flair",
|
|
38
|
+
"memory",
|
|
39
|
+
"semantic-search",
|
|
40
|
+
"ai",
|
|
41
|
+
"agent"
|
|
42
|
+
],
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@mariozechner/pi-coding-agent": "0.73.0",
|
|
45
|
+
"@types/node": "^24.0.0",
|
|
46
|
+
"typescript": "^5.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|