pi-continuous-learning 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.
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/active-instincts.d.ts +4 -0
- package/dist/active-instincts.d.ts.map +1 -0
- package/dist/active-instincts.js +11 -0
- package/dist/active-instincts.js.map +1 -0
- package/dist/agents-md.d.ts +12 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +23 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/cli/analyze-prompt.d.ts +2 -0
- package/dist/cli/analyze-prompt.d.ts.map +1 -0
- package/dist/cli/analyze-prompt.js +72 -0
- package/dist/cli/analyze-prompt.js.map +1 -0
- package/dist/cli/analyze.d.ts +3 -0
- package/dist/cli/analyze.d.ts.map +1 -0
- package/dist/cli/analyze.js +214 -0
- package/dist/cli/analyze.js.map +1 -0
- package/dist/confidence.d.ts +25 -0
- package/dist/confidence.d.ts.map +1 -0
- package/dist/confidence.js +77 -0
- package/dist/confidence.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +89 -0
- package/dist/config.js.map +1 -0
- package/dist/error-logger.d.ts +34 -0
- package/dist/error-logger.d.ts.map +1 -0
- package/dist/error-logger.js +102 -0
- package/dist/error-logger.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -0
- package/dist/instinct-decay.d.ts +39 -0
- package/dist/instinct-decay.d.ts.map +1 -0
- package/dist/instinct-decay.js +93 -0
- package/dist/instinct-decay.js.map +1 -0
- package/dist/instinct-evolve.d.ts +5 -0
- package/dist/instinct-evolve.d.ts.map +1 -0
- package/dist/instinct-evolve.js +24 -0
- package/dist/instinct-evolve.js.map +1 -0
- package/dist/instinct-export.d.ts +40 -0
- package/dist/instinct-export.d.ts.map +1 -0
- package/dist/instinct-export.js +94 -0
- package/dist/instinct-export.js.map +1 -0
- package/dist/instinct-import.d.ts +50 -0
- package/dist/instinct-import.d.ts.map +1 -0
- package/dist/instinct-import.js +168 -0
- package/dist/instinct-import.js.map +1 -0
- package/dist/instinct-injector.d.ts +39 -0
- package/dist/instinct-injector.d.ts.map +1 -0
- package/dist/instinct-injector.js +89 -0
- package/dist/instinct-injector.js.map +1 -0
- package/dist/instinct-loader.d.ts +37 -0
- package/dist/instinct-loader.d.ts.map +1 -0
- package/dist/instinct-loader.js +96 -0
- package/dist/instinct-loader.js.map +1 -0
- package/dist/instinct-parser.d.ts +28 -0
- package/dist/instinct-parser.d.ts.map +1 -0
- package/dist/instinct-parser.js +143 -0
- package/dist/instinct-parser.js.map +1 -0
- package/dist/instinct-projects.d.ts +32 -0
- package/dist/instinct-projects.d.ts.map +1 -0
- package/dist/instinct-projects.js +96 -0
- package/dist/instinct-projects.js.map +1 -0
- package/dist/instinct-promote.d.ts +51 -0
- package/dist/instinct-promote.d.ts.map +1 -0
- package/dist/instinct-promote.js +169 -0
- package/dist/instinct-promote.js.map +1 -0
- package/dist/instinct-status.d.ts +39 -0
- package/dist/instinct-status.d.ts.map +1 -0
- package/dist/instinct-status.js +108 -0
- package/dist/instinct-status.js.map +1 -0
- package/dist/instinct-store.d.ts +30 -0
- package/dist/instinct-store.d.ts.map +1 -0
- package/dist/instinct-store.js +118 -0
- package/dist/instinct-store.js.map +1 -0
- package/dist/instinct-tools.d.ts +161 -0
- package/dist/instinct-tools.d.ts.map +1 -0
- package/dist/instinct-tools.js +240 -0
- package/dist/instinct-tools.js.map +1 -0
- package/dist/observations.d.ts +22 -0
- package/dist/observations.d.ts.map +1 -0
- package/dist/observations.js +62 -0
- package/dist/observations.js.map +1 -0
- package/dist/observer-guard.d.ts +3 -0
- package/dist/observer-guard.d.ts.map +1 -0
- package/dist/observer-guard.js +13 -0
- package/dist/observer-guard.js.map +1 -0
- package/dist/project.d.ts +16 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +59 -0
- package/dist/project.js.map +1 -0
- package/dist/prompt-observer.d.ts +25 -0
- package/dist/prompt-observer.d.ts.map +1 -0
- package/dist/prompt-observer.js +63 -0
- package/dist/prompt-observer.js.map +1 -0
- package/dist/prompts/analyzer-user.d.ts +38 -0
- package/dist/prompts/analyzer-user.d.ts.map +1 -0
- package/dist/prompts/analyzer-user.js +105 -0
- package/dist/prompts/analyzer-user.js.map +1 -0
- package/dist/prompts/evolve-prompt.d.ts +3 -0
- package/dist/prompts/evolve-prompt.d.ts.map +1 -0
- package/dist/prompts/evolve-prompt.js +51 -0
- package/dist/prompts/evolve-prompt.js.map +1 -0
- package/dist/scrubber.d.ts +9 -0
- package/dist/scrubber.d.ts.map +1 -0
- package/dist/scrubber.js +40 -0
- package/dist/scrubber.js.map +1 -0
- package/dist/storage.d.ts +22 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +71 -0
- package/dist/storage.js.map +1 -0
- package/dist/tool-observer.d.ts +32 -0
- package/dist/tool-observer.d.ts.map +1 -0
- package/dist/tool-observer.js +77 -0
- package/dist/tool-observer.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
- package/src/active-instincts.ts +13 -0
- package/src/agents-md.ts +23 -0
- package/src/cli/analyze-prompt.ts +71 -0
- package/src/cli/analyze.ts +286 -0
- package/src/confidence.ts +103 -0
- package/src/config.ts +111 -0
- package/src/error-logger.ts +130 -0
- package/src/index.ts +144 -0
- package/src/instinct-decay.ts +117 -0
- package/src/instinct-evolve.ts +44 -0
- package/src/instinct-export.ts +138 -0
- package/src/instinct-import.ts +260 -0
- package/src/instinct-injector.ts +128 -0
- package/src/instinct-loader.ts +146 -0
- package/src/instinct-parser.ts +171 -0
- package/src/instinct-projects.ts +119 -0
- package/src/instinct-promote.ts +231 -0
- package/src/instinct-status.ts +135 -0
- package/src/instinct-store.ts +149 -0
- package/src/instinct-tools.ts +340 -0
- package/src/observations.ts +82 -0
- package/src/observer-guard.ts +14 -0
- package/src/project.ts +70 -0
- package/src/prompt-observer.ts +92 -0
- package/src/prompts/analyzer-user.ts +156 -0
- package/src/prompts/evolve-prompt.ts +71 -0
- package/src/scrubber.ts +42 -0
- package/src/storage.ts +91 -0
- package/src/tool-observer.ts +114 -0
- package/src/types.ts +90 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instinct CRUD operations.
|
|
3
|
+
* Provides functions to load, save, list, and delete instinct files from disk.
|
|
4
|
+
* Path traversal prevention: instinct IDs must be kebab-case (no ".." possible).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import type { Instinct } from "./types.js";
|
|
10
|
+
import { parseInstinct, serializeInstinct } from "./instinct-parser.js";
|
|
11
|
+
import {
|
|
12
|
+
getBaseDir,
|
|
13
|
+
getProjectInstinctsDir,
|
|
14
|
+
getGlobalInstinctsDir,
|
|
15
|
+
} from "./storage.js";
|
|
16
|
+
|
|
17
|
+
const INSTINCT_EXTENSION = ".md";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Mtime-based cache
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const CACHE_TTL_MS = 5_000;
|
|
24
|
+
|
|
25
|
+
interface DirCache {
|
|
26
|
+
instincts: Instinct[];
|
|
27
|
+
maxMtime: number;
|
|
28
|
+
checkedAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const dirCache = new Map<string, DirCache>();
|
|
32
|
+
|
|
33
|
+
export function invalidateCache(dir?: string): void {
|
|
34
|
+
if (dir) {
|
|
35
|
+
dirCache.delete(dir);
|
|
36
|
+
} else {
|
|
37
|
+
dirCache.clear();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getMaxMtime(dir: string): number {
|
|
42
|
+
if (!existsSync(dir)) return 0;
|
|
43
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(INSTINCT_EXTENSION));
|
|
44
|
+
let max = 0;
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
try {
|
|
47
|
+
const mt = statSync(join(dir, file)).mtimeMs;
|
|
48
|
+
if (mt > max) max = mt;
|
|
49
|
+
} catch {
|
|
50
|
+
// skip files that disappeared between readdir and stat
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return max;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function listInstinctsCached(dir: string): Instinct[] {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const cached = dirCache.get(dir);
|
|
59
|
+
|
|
60
|
+
if (cached && now - cached.checkedAt < CACHE_TTL_MS) {
|
|
61
|
+
return cached.instincts;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const currentMaxMtime = getMaxMtime(dir);
|
|
65
|
+
|
|
66
|
+
if (cached && currentMaxMtime === cached.maxMtime && currentMaxMtime > 0) {
|
|
67
|
+
dirCache.set(dir, { ...cached, checkedAt: now });
|
|
68
|
+
return cached.instincts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const instincts = listInstincts(dir);
|
|
72
|
+
dirCache.set(dir, { instincts, maxMtime: currentMaxMtime, checkedAt: now });
|
|
73
|
+
return instincts;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Guard against path traversal attacks in instinct IDs.
|
|
78
|
+
* Kebab-case validation in parseInstinct already prevents "..", but this
|
|
79
|
+
* provides an explicit defense for direct callers of save/load by file path.
|
|
80
|
+
*/
|
|
81
|
+
function assertNoPathTraversal(id: string): void {
|
|
82
|
+
if (id.includes("..") || id.includes("/") || id.includes("\\")) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Invalid instinct ID "${id}": path traversal characters are not allowed.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load a single instinct from a .md file path.
|
|
91
|
+
*/
|
|
92
|
+
export function loadInstinct(filePath: string): Instinct {
|
|
93
|
+
const content = readFileSync(filePath, "utf-8");
|
|
94
|
+
return parseInstinct(content);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Save an instinct to <dir>/<id>.md.
|
|
99
|
+
* Validates the instinct ID against path traversal before writing.
|
|
100
|
+
*/
|
|
101
|
+
export function saveInstinct(instinct: Instinct, dir: string): void {
|
|
102
|
+
assertNoPathTraversal(instinct.id);
|
|
103
|
+
const filePath = join(dir, `${instinct.id}${INSTINCT_EXTENSION}`);
|
|
104
|
+
const content = serializeInstinct(instinct);
|
|
105
|
+
writeFileSync(filePath, content, "utf-8");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* List and load all instincts from a directory.
|
|
110
|
+
* Silently skips files that fail to parse (malformed instinct files).
|
|
111
|
+
*/
|
|
112
|
+
export function listInstincts(dir: string): Instinct[] {
|
|
113
|
+
if (!existsSync(dir)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(INSTINCT_EXTENSION));
|
|
118
|
+
const instincts: Instinct[] = [];
|
|
119
|
+
|
|
120
|
+
for (const file of files) {
|
|
121
|
+
try {
|
|
122
|
+
const instinct = loadInstinct(join(dir, file));
|
|
123
|
+
instincts.push(instinct);
|
|
124
|
+
} catch {
|
|
125
|
+
// Skip malformed instinct files - do not crash the caller
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return instincts;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load all personal instincts for a specific project.
|
|
134
|
+
*/
|
|
135
|
+
export function loadProjectInstincts(
|
|
136
|
+
projectId: string,
|
|
137
|
+
baseDir = getBaseDir()
|
|
138
|
+
): Instinct[] {
|
|
139
|
+
const dir = getProjectInstinctsDir(projectId, "personal", baseDir);
|
|
140
|
+
return listInstinctsCached(dir);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Load all global personal instincts.
|
|
145
|
+
*/
|
|
146
|
+
export function loadGlobalInstincts(baseDir = getBaseDir()): Instinct[] {
|
|
147
|
+
const dir = getGlobalInstinctsDir("personal", baseDir);
|
|
148
|
+
return listInstinctsCached(dir);
|
|
149
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
3
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
4
|
+
import { unlinkSync, existsSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { Instinct } from "./types.js";
|
|
7
|
+
import {
|
|
8
|
+
loadProjectInstincts,
|
|
9
|
+
loadGlobalInstincts,
|
|
10
|
+
saveInstinct,
|
|
11
|
+
loadInstinct,
|
|
12
|
+
} from "./instinct-store.js";
|
|
13
|
+
import {
|
|
14
|
+
getProjectInstinctsDir,
|
|
15
|
+
getGlobalInstinctsDir,
|
|
16
|
+
} from "./storage.js";
|
|
17
|
+
|
|
18
|
+
function getInstinctsDir(
|
|
19
|
+
scope: "project" | "global",
|
|
20
|
+
projectId: string | null,
|
|
21
|
+
baseDir?: string
|
|
22
|
+
): string | null {
|
|
23
|
+
if (scope === "project") {
|
|
24
|
+
return projectId ? getProjectInstinctsDir(projectId, "personal", baseDir) : null;
|
|
25
|
+
}
|
|
26
|
+
return getGlobalInstinctsDir("personal", baseDir);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findInstinctFile(
|
|
30
|
+
id: string,
|
|
31
|
+
projectId: string | null,
|
|
32
|
+
baseDir?: string
|
|
33
|
+
): { path: string; scope: "project" | "global" } | null {
|
|
34
|
+
if (projectId) {
|
|
35
|
+
const projDir = getProjectInstinctsDir(projectId, "personal", baseDir);
|
|
36
|
+
const projPath = join(projDir, `${id}.md`);
|
|
37
|
+
if (existsSync(projPath)) return { path: projPath, scope: "project" };
|
|
38
|
+
}
|
|
39
|
+
const globalDir = getGlobalInstinctsDir("personal", baseDir);
|
|
40
|
+
const globalPath = join(globalDir, `${id}.md`);
|
|
41
|
+
if (existsSync(globalPath)) return { path: globalPath, scope: "global" };
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatInstinct(i: Instinct): string {
|
|
46
|
+
return `[${i.confidence.toFixed(2)}] ${i.id} (${i.domain}, ${i.scope})\n Trigger: ${i.trigger}\n Action: ${i.action}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ListParams = Type.Object({
|
|
50
|
+
scope: Type.Optional(StringEnum(["project", "global", "all"] as const)),
|
|
51
|
+
domain: Type.Optional(Type.String({ description: "Filter by domain (e.g. typescript, git, workflow)" })),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const ReadParams = Type.Object({
|
|
55
|
+
id: Type.String({ description: "Instinct ID (kebab-case)" }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const WriteParams = Type.Object({
|
|
59
|
+
id: Type.String({ description: "Instinct ID (kebab-case)" }),
|
|
60
|
+
title: Type.String(),
|
|
61
|
+
trigger: Type.String({ description: "When this instinct should activate" }),
|
|
62
|
+
action: Type.String({ description: "What the agent should do" }),
|
|
63
|
+
confidence: Type.Number({ minimum: 0.1, maximum: 0.9 }),
|
|
64
|
+
domain: Type.String(),
|
|
65
|
+
scope: StringEnum(["project", "global"] as const),
|
|
66
|
+
observation_count: Type.Optional(Type.Number({ default: 1 })),
|
|
67
|
+
confirmed_count: Type.Optional(Type.Number({ default: 0 })),
|
|
68
|
+
contradicted_count: Type.Optional(Type.Number({ default: 0 })),
|
|
69
|
+
inactive_count: Type.Optional(Type.Number({ default: 0 })),
|
|
70
|
+
evidence: Type.Optional(Type.Array(Type.String())),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const DeleteParams = Type.Object({
|
|
74
|
+
id: Type.String({ description: "Instinct ID to delete" }),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const MergeParams = Type.Object({
|
|
78
|
+
merged: Type.Object({
|
|
79
|
+
id: Type.String(),
|
|
80
|
+
title: Type.String(),
|
|
81
|
+
trigger: Type.String(),
|
|
82
|
+
action: Type.String(),
|
|
83
|
+
confidence: Type.Number({ minimum: 0.1, maximum: 0.9 }),
|
|
84
|
+
domain: Type.String(),
|
|
85
|
+
scope: StringEnum(["project", "global"] as const),
|
|
86
|
+
evidence: Type.Optional(Type.Array(Type.String())),
|
|
87
|
+
}),
|
|
88
|
+
delete_ids: Type.Array(Type.String(), { description: "IDs of source instincts to remove after merge" }),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export type InstinctListInput = Static<typeof ListParams>;
|
|
92
|
+
export type InstinctReadInput = Static<typeof ReadParams>;
|
|
93
|
+
export type InstinctWriteInput = Static<typeof WriteParams>;
|
|
94
|
+
export type InstinctDeleteInput = Static<typeof DeleteParams>;
|
|
95
|
+
export type InstinctMergeInput = Static<typeof MergeParams>;
|
|
96
|
+
|
|
97
|
+
export function createInstinctWriteTool(
|
|
98
|
+
projectId: string | null,
|
|
99
|
+
projectName: string | null,
|
|
100
|
+
baseDir?: string
|
|
101
|
+
) {
|
|
102
|
+
return {
|
|
103
|
+
name: "instinct_write" as const,
|
|
104
|
+
label: "Write Instinct",
|
|
105
|
+
description: "Create or update a learned behavior instinct",
|
|
106
|
+
promptSnippet: "Create or update a learned behavior instinct (trigger + action pattern)",
|
|
107
|
+
parameters: WriteParams,
|
|
108
|
+
async execute(
|
|
109
|
+
_toolCallId: string,
|
|
110
|
+
params: InstinctWriteInput,
|
|
111
|
+
_signal: AbortSignal | undefined,
|
|
112
|
+
_onUpdate: unknown,
|
|
113
|
+
_ctx: unknown
|
|
114
|
+
) {
|
|
115
|
+
const dir = getInstinctsDir(params.scope, projectId, baseDir);
|
|
116
|
+
if (!dir) {
|
|
117
|
+
throw new Error("Cannot write project-scoped instinct: no project detected");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
const existing = findInstinctFile(params.id, projectId, baseDir);
|
|
122
|
+
|
|
123
|
+
const instinct: Instinct = {
|
|
124
|
+
id: params.id,
|
|
125
|
+
title: params.title,
|
|
126
|
+
trigger: params.trigger,
|
|
127
|
+
action: params.action,
|
|
128
|
+
confidence: params.confidence,
|
|
129
|
+
domain: params.domain,
|
|
130
|
+
source: "personal",
|
|
131
|
+
scope: params.scope,
|
|
132
|
+
...(params.scope === "project" && projectId ? { project_id: projectId } : {}),
|
|
133
|
+
...(params.scope === "project" && projectName ? { project_name: projectName } : {}),
|
|
134
|
+
created_at: existing ? loadInstinct(existing.path).created_at : now,
|
|
135
|
+
updated_at: now,
|
|
136
|
+
observation_count: params.observation_count ?? 1,
|
|
137
|
+
confirmed_count: params.confirmed_count ?? 0,
|
|
138
|
+
contradicted_count: params.contradicted_count ?? 0,
|
|
139
|
+
inactive_count: params.inactive_count ?? 0,
|
|
140
|
+
...(params.evidence ? { evidence: params.evidence } : {}),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
saveInstinct(instinct, dir);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: "text" as const, text: `${existing ? "Updated" : "Created"} instinct: ${params.id}` }],
|
|
147
|
+
details: { id: params.id, action: existing ? "updated" : "created" },
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function createInstinctReadTool(
|
|
154
|
+
projectId: string | null,
|
|
155
|
+
baseDir?: string
|
|
156
|
+
) {
|
|
157
|
+
return {
|
|
158
|
+
name: "instinct_read" as const,
|
|
159
|
+
label: "Read Instinct",
|
|
160
|
+
description: "Read a specific instinct by ID",
|
|
161
|
+
promptSnippet: "Read the full details of a specific learned instinct by ID",
|
|
162
|
+
parameters: ReadParams,
|
|
163
|
+
async execute(
|
|
164
|
+
_toolCallId: string,
|
|
165
|
+
params: InstinctReadInput,
|
|
166
|
+
_signal: AbortSignal | undefined,
|
|
167
|
+
_onUpdate: unknown,
|
|
168
|
+
_ctx: unknown
|
|
169
|
+
) {
|
|
170
|
+
const found = findInstinctFile(params.id, projectId, baseDir);
|
|
171
|
+
if (!found) {
|
|
172
|
+
throw new Error(`Instinct not found: ${params.id}`);
|
|
173
|
+
}
|
|
174
|
+
const instinct = loadInstinct(found.path);
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text" as const, text: formatInstinct(instinct) }],
|
|
177
|
+
details: instinct,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createInstinctListTool(
|
|
184
|
+
projectId: string | null,
|
|
185
|
+
baseDir?: string
|
|
186
|
+
) {
|
|
187
|
+
return {
|
|
188
|
+
name: "instinct_list" as const,
|
|
189
|
+
label: "List Instincts",
|
|
190
|
+
description: "List learned behavior instincts with optional filters",
|
|
191
|
+
promptSnippet: "List all learned instincts, optionally filtered by scope or domain",
|
|
192
|
+
parameters: ListParams,
|
|
193
|
+
async execute(
|
|
194
|
+
_toolCallId: string,
|
|
195
|
+
params: InstinctListInput,
|
|
196
|
+
_signal: AbortSignal | undefined,
|
|
197
|
+
_onUpdate: unknown,
|
|
198
|
+
_ctx: unknown
|
|
199
|
+
) {
|
|
200
|
+
const scope = params.scope ?? "all";
|
|
201
|
+
let instincts: Instinct[] = [];
|
|
202
|
+
|
|
203
|
+
if ((scope === "project" || scope === "all") && projectId) {
|
|
204
|
+
instincts.push(...loadProjectInstincts(projectId, baseDir));
|
|
205
|
+
}
|
|
206
|
+
if (scope === "global" || scope === "all") {
|
|
207
|
+
instincts.push(...loadGlobalInstincts(baseDir));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (params.domain) {
|
|
211
|
+
const domain = params.domain.toLowerCase();
|
|
212
|
+
instincts = instincts.filter((i) => i.domain.toLowerCase() === domain);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
instincts.sort((a, b) => b.confidence - a.confidence);
|
|
216
|
+
|
|
217
|
+
if (instincts.length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: "text" as const, text: "No instincts found matching the given filters." }],
|
|
220
|
+
details: { count: 0 },
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const text = instincts.map(formatInstinct).join("\n\n");
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: "text" as const, text: `${instincts.length} instinct(s):\n\n${text}` }],
|
|
227
|
+
details: { count: instincts.length },
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function createInstinctDeleteTool(
|
|
234
|
+
projectId: string | null,
|
|
235
|
+
baseDir?: string
|
|
236
|
+
) {
|
|
237
|
+
return {
|
|
238
|
+
name: "instinct_delete" as const,
|
|
239
|
+
label: "Delete Instinct",
|
|
240
|
+
description: "Remove a learned instinct by ID",
|
|
241
|
+
promptSnippet: "Delete a learned instinct permanently by ID",
|
|
242
|
+
parameters: DeleteParams,
|
|
243
|
+
async execute(
|
|
244
|
+
_toolCallId: string,
|
|
245
|
+
params: InstinctDeleteInput,
|
|
246
|
+
_signal: AbortSignal | undefined,
|
|
247
|
+
_onUpdate: unknown,
|
|
248
|
+
_ctx: unknown
|
|
249
|
+
) {
|
|
250
|
+
const found = findInstinctFile(params.id, projectId, baseDir);
|
|
251
|
+
if (!found) {
|
|
252
|
+
throw new Error(`Instinct not found: ${params.id}`);
|
|
253
|
+
}
|
|
254
|
+
unlinkSync(found.path);
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: "text" as const, text: `Deleted instinct: ${params.id} (was ${found.scope}-scoped)` }],
|
|
257
|
+
details: { id: params.id, scope: found.scope },
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function createInstinctMergeTool(
|
|
264
|
+
projectId: string | null,
|
|
265
|
+
projectName: string | null,
|
|
266
|
+
baseDir?: string
|
|
267
|
+
) {
|
|
268
|
+
return {
|
|
269
|
+
name: "instinct_merge" as const,
|
|
270
|
+
label: "Merge Instincts",
|
|
271
|
+
description: "Merge multiple instincts into one, removing the originals",
|
|
272
|
+
promptSnippet: "Merge multiple related instincts into a single consolidated instinct",
|
|
273
|
+
parameters: MergeParams,
|
|
274
|
+
async execute(
|
|
275
|
+
_toolCallId: string,
|
|
276
|
+
params: InstinctMergeInput,
|
|
277
|
+
_signal: AbortSignal | undefined,
|
|
278
|
+
_onUpdate: unknown,
|
|
279
|
+
_ctx: unknown
|
|
280
|
+
) {
|
|
281
|
+
const { merged, delete_ids } = params;
|
|
282
|
+
const dir = getInstinctsDir(merged.scope, projectId, baseDir);
|
|
283
|
+
if (!dir) {
|
|
284
|
+
throw new Error("Cannot write project-scoped instinct: no project detected");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const now = new Date().toISOString();
|
|
288
|
+
const instinct: Instinct = {
|
|
289
|
+
...merged,
|
|
290
|
+
source: "personal",
|
|
291
|
+
...(merged.scope === "project" && projectId ? { project_id: projectId } : {}),
|
|
292
|
+
...(merged.scope === "project" && projectName ? { project_name: projectName } : {}),
|
|
293
|
+
created_at: now,
|
|
294
|
+
updated_at: now,
|
|
295
|
+
observation_count: 0,
|
|
296
|
+
confirmed_count: 0,
|
|
297
|
+
contradicted_count: 0,
|
|
298
|
+
inactive_count: 0,
|
|
299
|
+
...(merged.evidence ? { evidence: merged.evidence } : {}),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
saveInstinct(instinct, dir);
|
|
303
|
+
|
|
304
|
+
const deleted: string[] = [];
|
|
305
|
+
for (const id of delete_ids) {
|
|
306
|
+
if (id === merged.id) continue;
|
|
307
|
+
const found = findInstinctFile(id, projectId, baseDir);
|
|
308
|
+
if (found) {
|
|
309
|
+
unlinkSync(found.path);
|
|
310
|
+
deleted.push(id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
content: [{
|
|
316
|
+
type: "text" as const,
|
|
317
|
+
text: `Merged into ${merged.id}. Deleted ${deleted.length} source instinct(s): ${deleted.join(", ")}`,
|
|
318
|
+
}],
|
|
319
|
+
details: { mergedId: merged.id, deleted },
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function registerAllTools(
|
|
326
|
+
pi: ExtensionAPI,
|
|
327
|
+
projectId: string | null,
|
|
328
|
+
projectName: string | null,
|
|
329
|
+
baseDir?: string
|
|
330
|
+
): void {
|
|
331
|
+
const guidelines = [
|
|
332
|
+
"Use instinct tools when the user asks about learned behaviors, patterns, or instincts.",
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
pi.registerTool({ ...createInstinctListTool(projectId, baseDir), promptGuidelines: guidelines });
|
|
336
|
+
pi.registerTool({ ...createInstinctReadTool(projectId, baseDir), promptGuidelines: guidelines });
|
|
337
|
+
pi.registerTool({ ...createInstinctWriteTool(projectId, projectName, baseDir), promptGuidelines: guidelines });
|
|
338
|
+
pi.registerTool({ ...createInstinctDeleteTool(projectId, baseDir), promptGuidelines: guidelines });
|
|
339
|
+
pi.registerTool({ ...createInstinctMergeTool(projectId, projectName, baseDir), promptGuidelines: guidelines });
|
|
340
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL observation file writing and archival.
|
|
3
|
+
* Appends observations to per-project observations.jsonl files,
|
|
4
|
+
* archives files at 10MB, and cleans up archives older than 30 days.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
appendFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
statSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import type { Observation } from "./types.js";
|
|
18
|
+
import { getArchiveDir, getObservationsPath } from "./storage.js";
|
|
19
|
+
|
|
20
|
+
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
21
|
+
const MAX_ARCHIVE_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
22
|
+
|
|
23
|
+
function isFileSizeExceeded(filePath: string): boolean {
|
|
24
|
+
if (!existsSync(filePath)) return false;
|
|
25
|
+
return statSync(filePath).size >= MAX_FILE_SIZE_BYTES;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function archiveFile(observationsPath: string, archiveDir: string): void {
|
|
29
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
30
|
+
const archivePath = join(archiveDir, `${timestamp}.jsonl`);
|
|
31
|
+
renameSync(observationsPath, archivePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Appends one observation as a JSON line to the project's observations.jsonl.
|
|
36
|
+
* Automatically archives the file if it has grown to or beyond 10MB.
|
|
37
|
+
*/
|
|
38
|
+
export function appendObservation(
|
|
39
|
+
observation: Observation,
|
|
40
|
+
projectId: string,
|
|
41
|
+
baseDir?: string
|
|
42
|
+
): void {
|
|
43
|
+
const observationsPath = getObservationsPath(projectId, baseDir);
|
|
44
|
+
const archiveDir = getArchiveDir(projectId, baseDir);
|
|
45
|
+
|
|
46
|
+
if (isFileSizeExceeded(observationsPath)) {
|
|
47
|
+
archiveFile(observationsPath, archiveDir);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
appendFileSync(observationsPath, JSON.stringify(observation) + "\n", "utf-8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deletes archive files older than 30 days.
|
|
55
|
+
* Should be called once per session_start.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Counts non-empty lines in the project's observations.jsonl file.
|
|
59
|
+
* Returns 0 when the file does not exist.
|
|
60
|
+
*/
|
|
61
|
+
export function countObservations(projectId: string, baseDir?: string): number {
|
|
62
|
+
const obsPath = getObservationsPath(projectId, baseDir);
|
|
63
|
+
if (!existsSync(obsPath)) return 0;
|
|
64
|
+
const content = readFileSync(obsPath, "utf-8") as string;
|
|
65
|
+
return content.split("\n").filter((line) => line.trim() !== "").length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function cleanOldArchives(projectId: string, baseDir?: string): void {
|
|
69
|
+
const archiveDir = getArchiveDir(projectId, baseDir);
|
|
70
|
+
if (!existsSync(archiveDir)) return;
|
|
71
|
+
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const files = readdirSync(archiveDir).filter((f) => f.endsWith(".jsonl"));
|
|
74
|
+
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const filePath = join(archiveDir, file);
|
|
77
|
+
const { mtimeMs } = statSync(filePath);
|
|
78
|
+
if (now - mtimeMs >= MAX_ARCHIVE_AGE_MS) {
|
|
79
|
+
unlinkSync(filePath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const LEARNING_BASE_DIR = join(homedir(), ".pi", "continuous-learning");
|
|
5
|
+
const LEARNING_BASE_DIR_PREFIX = LEARNING_BASE_DIR + "/";
|
|
6
|
+
|
|
7
|
+
export function shouldSkipPath(filePath: string): boolean {
|
|
8
|
+
return filePath === LEARNING_BASE_DIR || filePath.startsWith(LEARNING_BASE_DIR_PREFIX);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function shouldSkipObservation(filePath?: string): boolean {
|
|
12
|
+
if (filePath !== undefined && shouldSkipPath(filePath)) return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project detection via git remote URL hashing.
|
|
3
|
+
* Scopes observations and instincts to the correct project.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { ProjectEntry } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const GLOBAL_PROJECT_ID = "global";
|
|
12
|
+
const HASH_LENGTH = 12;
|
|
13
|
+
|
|
14
|
+
function hashString(input: string): string {
|
|
15
|
+
return createHash("sha256").update(input).digest("hex").substring(0, HASH_LENGTH);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect the current project by inspecting git remote URL.
|
|
20
|
+
*
|
|
21
|
+
* Resolution order:
|
|
22
|
+
* 1. git remote get-url origin -> hash of remote URL
|
|
23
|
+
* 2. git rev-parse --show-toplevel -> hash of repo root path
|
|
24
|
+
* 3. fallback to project ID "global"
|
|
25
|
+
*/
|
|
26
|
+
export async function detectProject(
|
|
27
|
+
pi: ExtensionAPI,
|
|
28
|
+
cwd: string
|
|
29
|
+
): Promise<ProjectEntry> {
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
const name = basename(cwd);
|
|
32
|
+
|
|
33
|
+
// 1. Try remote URL
|
|
34
|
+
const remoteResult = await pi.exec("git", ["remote", "get-url", "origin"], { cwd });
|
|
35
|
+
if (remoteResult.code === 0) {
|
|
36
|
+
const remote = remoteResult.stdout.trim();
|
|
37
|
+
return {
|
|
38
|
+
id: hashString(remote),
|
|
39
|
+
name,
|
|
40
|
+
root: cwd,
|
|
41
|
+
remote,
|
|
42
|
+
created_at: now,
|
|
43
|
+
last_seen: now,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Fallback: repo root path (no remote)
|
|
48
|
+
const rootResult = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
49
|
+
if (rootResult.code === 0) {
|
|
50
|
+
const root = rootResult.stdout.trim();
|
|
51
|
+
return {
|
|
52
|
+
id: hashString(root),
|
|
53
|
+
name,
|
|
54
|
+
root,
|
|
55
|
+
remote: "",
|
|
56
|
+
created_at: now,
|
|
57
|
+
last_seen: now,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Fallback: not in a git repo
|
|
62
|
+
return {
|
|
63
|
+
id: GLOBAL_PROJECT_ID,
|
|
64
|
+
name,
|
|
65
|
+
root: cwd,
|
|
66
|
+
remote: "",
|
|
67
|
+
created_at: now,
|
|
68
|
+
last_seen: now,
|
|
69
|
+
};
|
|
70
|
+
}
|