om-memory-system 3.0.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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +587 -0
  3. package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
  4. package/dist/adapters/opencode/auto-capture-host.js +29 -0
  5. package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
  6. package/dist/adapters/opencode/auto-capture-summary.js +147 -0
  7. package/dist/adapters/opencode/conversation.d.ts +2 -0
  8. package/dist/adapters/opencode/conversation.js +50 -0
  9. package/dist/adapters/opencode/user-prompt.d.ts +9 -0
  10. package/dist/adapters/opencode/user-prompt.js +26 -0
  11. package/dist/adapters/pi/capture.d.ts +41 -0
  12. package/dist/adapters/pi/capture.js +91 -0
  13. package/dist/adapters/pi/conversation.d.ts +38 -0
  14. package/dist/adapters/pi/conversation.js +118 -0
  15. package/dist/adapters/pi/extension.d.ts +18 -0
  16. package/dist/adapters/pi/extension.js +234 -0
  17. package/dist/adapters/pi/import-command.d.ts +29 -0
  18. package/dist/adapters/pi/import-command.js +232 -0
  19. package/dist/adapters/pi/profile.d.ts +39 -0
  20. package/dist/adapters/pi/profile.js +148 -0
  21. package/dist/adapters/pi/provider.d.ts +50 -0
  22. package/dist/adapters/pi/provider.js +77 -0
  23. package/dist/config.d.ts +290 -0
  24. package/dist/config.js +742 -0
  25. package/dist/core/capture-context.d.ts +4 -0
  26. package/dist/core/capture-context.js +132 -0
  27. package/dist/core/capture.d.ts +15 -0
  28. package/dist/core/capture.js +68 -0
  29. package/dist/core/extraction.d.ts +80 -0
  30. package/dist/core/extraction.js +140 -0
  31. package/dist/core/host.d.ts +52 -0
  32. package/dist/core/host.js +1 -0
  33. package/dist/core/memory-operations.d.ts +27 -0
  34. package/dist/core/memory-operations.js +247 -0
  35. package/dist/core/retrieval.d.ts +12 -0
  36. package/dist/core/retrieval.js +72 -0
  37. package/dist/importer/discovery.d.ts +31 -0
  38. package/dist/importer/discovery.js +88 -0
  39. package/dist/importer/importer.d.ts +87 -0
  40. package/dist/importer/importer.js +260 -0
  41. package/dist/importer/ledger.d.ts +44 -0
  42. package/dist/importer/ledger.js +137 -0
  43. package/dist/importer/session-loader.d.ts +19 -0
  44. package/dist/importer/session-loader.js +12 -0
  45. package/dist/index.d.ts +24 -0
  46. package/dist/index.js +601 -0
  47. package/dist/plugin.d.ts +9 -0
  48. package/dist/plugin.js +11 -0
  49. package/dist/services/ai/ai-provider-factory.d.ts +7 -0
  50. package/dist/services/ai/ai-provider-factory.js +41 -0
  51. package/dist/services/ai/api-key-placeholder.d.ts +1 -0
  52. package/dist/services/ai/api-key-placeholder.js +14 -0
  53. package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
  54. package/dist/services/ai/internal-capture-sessions.js +33 -0
  55. package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
  56. package/dist/services/ai/opencode-diagnostics.js +36 -0
  57. package/dist/services/ai/opencode-host-config.d.ts +10 -0
  58. package/dist/services/ai/opencode-host-config.js +51 -0
  59. package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
  60. package/dist/services/ai/opencode-provider-loader.js +4 -0
  61. package/dist/services/ai/opencode-provider.d.ts +92 -0
  62. package/dist/services/ai/opencode-provider.js +419 -0
  63. package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
  64. package/dist/services/ai/opencode-sdk-client.js +38 -0
  65. package/dist/services/ai/profile-llm-client.d.ts +2 -0
  66. package/dist/services/ai/profile-llm-client.js +27 -0
  67. package/dist/services/ai/provider-config.d.ts +17 -0
  68. package/dist/services/ai/provider-config.js +30 -0
  69. package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
  70. package/dist/services/ai/providers/anthropic-messages.js +220 -0
  71. package/dist/services/ai/providers/base-provider.d.ts +24 -0
  72. package/dist/services/ai/providers/base-provider.js +24 -0
  73. package/dist/services/ai/providers/google-gemini.d.ts +15 -0
  74. package/dist/services/ai/providers/google-gemini.js +228 -0
  75. package/dist/services/ai/providers/minimax.d.ts +35 -0
  76. package/dist/services/ai/providers/minimax.js +55 -0
  77. package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
  78. package/dist/services/ai/providers/openai-chat-completion.js +387 -0
  79. package/dist/services/ai/providers/openai-responses.d.ts +13 -0
  80. package/dist/services/ai/providers/openai-responses.js +182 -0
  81. package/dist/services/ai/providers/orcarouter.d.ts +51 -0
  82. package/dist/services/ai/providers/orcarouter.js +66 -0
  83. package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
  84. package/dist/services/ai/session/ai-session-manager.js +211 -0
  85. package/dist/services/ai/session/session-types.d.ts +42 -0
  86. package/dist/services/ai/session/session-types.js +1 -0
  87. package/dist/services/ai/tools/tool-schema.d.ts +40 -0
  88. package/dist/services/ai/tools/tool-schema.js +24 -0
  89. package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
  90. package/dist/services/ai/validators/user-profile-validator.js +114 -0
  91. package/dist/services/api-handlers.d.ts +186 -0
  92. package/dist/services/api-handlers.js +1284 -0
  93. package/dist/services/auth-token.d.ts +14 -0
  94. package/dist/services/auth-token.js +59 -0
  95. package/dist/services/auto-capture.d.ts +3 -0
  96. package/dist/services/auto-capture.js +127 -0
  97. package/dist/services/cleanup-service.d.ts +22 -0
  98. package/dist/services/cleanup-service.js +109 -0
  99. package/dist/services/client.d.ts +134 -0
  100. package/dist/services/client.js +327 -0
  101. package/dist/services/context.d.ts +10 -0
  102. package/dist/services/context.js +27 -0
  103. package/dist/services/cors.d.ts +12 -0
  104. package/dist/services/cors.js +52 -0
  105. package/dist/services/deduplication-service.d.ts +28 -0
  106. package/dist/services/deduplication-service.js +111 -0
  107. package/dist/services/embedding.d.ts +36 -0
  108. package/dist/services/embedding.js +200 -0
  109. package/dist/services/jsonc.d.ts +6 -0
  110. package/dist/services/jsonc.js +76 -0
  111. package/dist/services/language-detector.d.ts +2 -0
  112. package/dist/services/language-detector.js +33 -0
  113. package/dist/services/legacy-migration.d.ts +77 -0
  114. package/dist/services/legacy-migration.js +343 -0
  115. package/dist/services/logger.d.ts +1 -0
  116. package/dist/services/logger.js +119 -0
  117. package/dist/services/memory-portability-service.d.ts +37 -0
  118. package/dist/services/memory-portability-service.js +340 -0
  119. package/dist/services/memory-scope.d.ts +16 -0
  120. package/dist/services/memory-scope.js +43 -0
  121. package/dist/services/migration-service.d.ts +42 -0
  122. package/dist/services/migration-service.js +307 -0
  123. package/dist/services/onnxruntime-resolve.d.ts +24 -0
  124. package/dist/services/onnxruntime-resolve.js +174 -0
  125. package/dist/services/privacy.d.ts +26 -0
  126. package/dist/services/privacy.js +69 -0
  127. package/dist/services/request-timeouts.d.ts +9 -0
  128. package/dist/services/request-timeouts.js +11 -0
  129. package/dist/services/runtime-require.d.ts +17 -0
  130. package/dist/services/runtime-require.js +75 -0
  131. package/dist/services/secret-resolver.d.ts +1 -0
  132. package/dist/services/secret-resolver.js +65 -0
  133. package/dist/services/shard-inventory-service.d.ts +69 -0
  134. package/dist/services/shard-inventory-service.js +315 -0
  135. package/dist/services/shard-path-migration-service.d.ts +31 -0
  136. package/dist/services/shard-path-migration-service.js +367 -0
  137. package/dist/services/tag-prefix-migration.d.ts +60 -0
  138. package/dist/services/tag-prefix-migration.js +439 -0
  139. package/dist/services/tag-prefix-scan.d.ts +26 -0
  140. package/dist/services/tag-prefix-scan.js +91 -0
  141. package/dist/services/tags.d.ts +29 -0
  142. package/dist/services/tags.js +251 -0
  143. package/dist/services/turso/connection-manager.d.ts +15 -0
  144. package/dist/services/turso/connection-manager.js +163 -0
  145. package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
  146. package/dist/services/turso/cross-process-write-lock.js +89 -0
  147. package/dist/services/turso/legacy-migrator.d.ts +1 -0
  148. package/dist/services/turso/legacy-migrator.js +540 -0
  149. package/dist/services/turso/lifecycle.d.ts +1 -0
  150. package/dist/services/turso/lifecycle.js +14 -0
  151. package/dist/services/turso/operation-lock.d.ts +2 -0
  152. package/dist/services/turso/operation-lock.js +61 -0
  153. package/dist/services/turso/ready.d.ts +2 -0
  154. package/dist/services/turso/ready.js +36 -0
  155. package/dist/services/turso/shard-manager.d.ts +38 -0
  156. package/dist/services/turso/shard-manager.js +496 -0
  157. package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
  158. package/dist/services/turso/sqlite-handle-release.js +56 -0
  159. package/dist/services/turso/turso-db.d.ts +18 -0
  160. package/dist/services/turso/turso-db.js +53 -0
  161. package/dist/services/turso/types.d.ts +42 -0
  162. package/dist/services/turso/types.js +1 -0
  163. package/dist/services/turso/vector-search.d.ts +41 -0
  164. package/dist/services/turso/vector-search.js +384 -0
  165. package/dist/services/turso/vector-utils.d.ts +7 -0
  166. package/dist/services/turso/vector-utils.js +67 -0
  167. package/dist/services/user-memory-learning.d.ts +97 -0
  168. package/dist/services/user-memory-learning.js +673 -0
  169. package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
  170. package/dist/services/user-profile/ai-cleanup.js +486 -0
  171. package/dist/services/user-profile/profile-context.d.ts +1 -0
  172. package/dist/services/user-profile/profile-context.js +105 -0
  173. package/dist/services/user-profile/profile-utils.d.ts +2 -0
  174. package/dist/services/user-profile/profile-utils.js +45 -0
  175. package/dist/services/user-profile/types.d.ts +92 -0
  176. package/dist/services/user-profile/types.js +1 -0
  177. package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
  178. package/dist/services/user-profile/user-profile-manager.js +1660 -0
  179. package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
  180. package/dist/services/user-prompt/user-prompt-manager.js +303 -0
  181. package/dist/services/web-api-auth.d.ts +3 -0
  182. package/dist/services/web-api-auth.js +25 -0
  183. package/dist/services/web-auth.d.ts +46 -0
  184. package/dist/services/web-auth.js +105 -0
  185. package/dist/services/web-server.d.ts +57 -0
  186. package/dist/services/web-server.js +659 -0
  187. package/dist/shared/api/index.d.ts +4 -0
  188. package/dist/shared/api/index.js +2 -0
  189. package/dist/shared/api/portability-schemas.d.ts +56 -0
  190. package/dist/shared/api/portability-schemas.js +39 -0
  191. package/dist/shared/api/schemas.d.ts +295 -0
  192. package/dist/shared/api/schemas.js +134 -0
  193. package/dist/shared/api/types.d.ts +28 -0
  194. package/dist/shared/api/types.js +1 -0
  195. package/dist/types/index.d.ts +27 -0
  196. package/dist/types/index.js +1 -0
  197. package/dist/utils/context-limit.d.ts +10 -0
  198. package/dist/utils/context-limit.js +43 -0
  199. package/dist/utils/math.d.ts +7 -0
  200. package/dist/utils/math.js +48 -0
  201. package/dist/utils/profile.d.ts +2 -0
  202. package/dist/utils/profile.js +15 -0
  203. package/dist/v2/adapter.d.ts +5 -0
  204. package/dist/v2/adapter.js +218 -0
  205. package/dist/v2/legacy-client.d.ts +15 -0
  206. package/dist/v2/legacy-client.js +257 -0
  207. package/dist/v2/memory-bridge.d.ts +18 -0
  208. package/dist/v2/memory-bridge.js +29 -0
  209. package/dist/v2/plugin.d.ts +3 -0
  210. package/dist/v2/plugin.js +24 -0
  211. package/dist/web/assets/index-DPjEpD1h.css +2 -0
  212. package/dist/web/assets/index-do26c4JJ.js +122 -0
  213. package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  214. package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  215. package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  216. package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  217. package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  218. package/dist/web/favicon.ico +0 -0
  219. package/dist/web/index.html +14 -0
  220. package/dist/web/omms-icon.png +0 -0
  221. package/package.json +138 -0
@@ -0,0 +1,234 @@
1
+ import { Type } from "typebox";
2
+ import { CONFIG, initConfigWithLegacyMigration, isConfigured } from "../../config.js";
3
+ import { executeMemoryOperation } from "../../core/memory-operations.js";
4
+ import { getLanguageName } from "../../services/language-detector.js";
5
+ import { log } from "../../services/logger.js";
6
+ import { memoryClient } from "../../services/client.js";
7
+ import { capturePiSettledWorkUnit, createPiCaptureState } from "./capture.js";
8
+ import { createPiCaptureProvider } from "./provider.js";
9
+ import { resolveModelFromContext } from "./provider.js";
10
+ import { registerPiHistoryImportCommand } from "./import-command.js";
11
+ import { performPiProfileLearning } from "./profile.js";
12
+ import { buildRetrievalSection, wrapRetrievalSection } from "../../core/retrieval.js";
13
+ const GLOBAL_PLUGIN_WARMUP_KEY = Symbol.for("omms.plugin.warmedup");
14
+ const OMMS_STATUS_KEY = "omms";
15
+ const MEMORY_TOOL_MODES = [
16
+ "add",
17
+ "search",
18
+ "profile",
19
+ "list",
20
+ "forget",
21
+ "help",
22
+ "migrate",
23
+ "list-shards",
24
+ "export",
25
+ "import",
26
+ ];
27
+ function toSessionEntries(branch) {
28
+ return Array.isArray(branch) ? branch : [];
29
+ }
30
+ function lastSettledUserPrompt(entries) {
31
+ for (let i = entries.length - 1; i >= 0; i--) {
32
+ const entry = entries[i];
33
+ if (entry.type !== "message" || entry.message?.role !== "user")
34
+ continue;
35
+ const content = entry.message.content;
36
+ if (typeof content === "string" && content.trim())
37
+ return content;
38
+ if (Array.isArray(content)) {
39
+ const text = content
40
+ .filter((block) => block.type === "text" && typeof block.text === "string")
41
+ .map((block) => block.text)
42
+ .join("\n")
43
+ .trim();
44
+ if (text)
45
+ return text;
46
+ }
47
+ return null;
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * Pi extension entry point for the shared omms engine.
53
+ *
54
+ * Lifecycle mapping:
55
+ * - `session_start` -> load shared config for ctx.cwd, warm storage
56
+ * - `before_agent_start` -> prompt-aware semantic retrieval, injected as a
57
+ * delimited system-prompt section (never a fake
58
+ * user message)
59
+ * - `agent_settled` -> automatic capture of the settled work unit
60
+ * - `session_shutdown` -> idempotent session-scoped cleanup
61
+ * - `memory` tool -> shared add/search/profile/list/forget/help and
62
+ * portability operations
63
+ *
64
+ * Pi types are imported type-only: the compiled extension carries no Pi
65
+ * runtime dependency and uses the host Pi runtime provided at load time.
66
+ */
67
+ export default function ommsPiExtension(pi) {
68
+ let captureState = createPiCaptureState();
69
+ let promptsSinceProfileAnalysis = [];
70
+ // Latest session context, refreshed on session_start and consumed by the
71
+ // import command (command contexts do not expose the model registry).
72
+ let latestCtx = null;
73
+ let statusVersion = 0;
74
+ const startStatus = (ctx, status) => {
75
+ const version = ++statusVersion;
76
+ if (ctx.hasUI)
77
+ ctx.ui.setStatus(OMMS_STATUS_KEY, `omms:${status}`);
78
+ return version;
79
+ };
80
+ const finishStatus = (ctx, version, status) => {
81
+ if (version !== statusVersion || !ctx.hasUI)
82
+ return;
83
+ ctx.ui.setStatus(OMMS_STATUS_KEY, `omms:${status}`);
84
+ };
85
+ const clearStatus = (ctx) => {
86
+ statusVersion++;
87
+ if (ctx.hasUI)
88
+ ctx.ui.setStatus(OMMS_STATUS_KEY, undefined);
89
+ };
90
+ const notify = (ctx) => (notification) => {
91
+ if (!ctx.hasUI)
92
+ return;
93
+ const level = notification.variant === "success" ? "info" : notification.variant;
94
+ ctx.ui.notify(`${notification.title}: ${notification.message}`, level);
95
+ };
96
+ const resolveModel = (ctx) => resolveModelFromContext(ctx);
97
+ pi.on("session_start", async (_event, ctx) => {
98
+ const status = startStatus(ctx, "warming");
99
+ try {
100
+ latestCtx = ctx;
101
+ initConfigWithLegacyMigration(ctx.cwd);
102
+ captureState = createPiCaptureState();
103
+ const globalScope = globalThis;
104
+ if (!globalScope[GLOBAL_PLUGIN_WARMUP_KEY] && isConfigured()) {
105
+ (async () => {
106
+ try {
107
+ await memoryClient.warmup();
108
+ globalScope[GLOBAL_PLUGIN_WARMUP_KEY] = true;
109
+ finishStatus(ctx, status, "connected");
110
+ }
111
+ catch (error) {
112
+ log("Pi plugin memory warmup failed", { error: String(error) });
113
+ finishStatus(ctx, status, "error");
114
+ }
115
+ })();
116
+ }
117
+ else {
118
+ finishStatus(ctx, status, "connected");
119
+ }
120
+ }
121
+ catch (error) {
122
+ log("Pi session_start error", { error: String(error) });
123
+ finishStatus(ctx, status, "error");
124
+ }
125
+ });
126
+ pi.on("before_agent_start", async (event, ctx) => {
127
+ if (!isConfigured() || !CONFIG.chatMessage.enabled)
128
+ return;
129
+ const status = startStatus(ctx, "recalling");
130
+ try {
131
+ const section = await buildRetrievalSection(event.prompt, ctx.cwd, ctx.sessionManager.getSessionId());
132
+ finishStatus(ctx, status, "connected");
133
+ if (!section)
134
+ return;
135
+ return {
136
+ systemPrompt: event.systemPrompt + "\n\n" + wrapRetrievalSection(section),
137
+ };
138
+ }
139
+ catch (error) {
140
+ log("Pi before_agent_start retrieval error", { error: String(error) });
141
+ finishStatus(ctx, status, "error");
142
+ return undefined;
143
+ }
144
+ });
145
+ pi.on("agent_settled", async (_event, ctx) => {
146
+ if (!isConfigured())
147
+ return;
148
+ const status = startStatus(ctx, "capturing");
149
+ try {
150
+ const entries = toSessionEntries(ctx.sessionManager.getBranch());
151
+ const provider = createPiCaptureProvider(() => resolveModel(ctx));
152
+ const captureResult = await capturePiSettledWorkUnit({
153
+ sessionId: ctx.sessionManager.getSessionId(),
154
+ directory: ctx.cwd,
155
+ entries,
156
+ provider,
157
+ state: captureState,
158
+ prompt: {
159
+ providerId: ctx.model?.provider ?? null,
160
+ modelId: ctx.model?.id ?? null,
161
+ },
162
+ notify: notify(ctx),
163
+ });
164
+ const settledPrompt = lastSettledUserPrompt(entries);
165
+ if (settledPrompt &&
166
+ CONFIG.userProfileAnalysisInterval > 0 &&
167
+ !memoryClient.getEmbeddingInitError?.()) {
168
+ promptsSinceProfileAnalysis.push(settledPrompt);
169
+ if (promptsSinceProfileAnalysis.length >= CONFIG.userProfileAnalysisInterval) {
170
+ const batch = promptsSinceProfileAnalysis;
171
+ promptsSinceProfileAnalysis = [];
172
+ await performPiProfileLearning({
173
+ directory: ctx.cwd,
174
+ prompts: batch,
175
+ resolveModel: () => resolveModel(ctx),
176
+ notify: notify(ctx),
177
+ });
178
+ }
179
+ }
180
+ finishStatus(ctx, status, captureResult.status === "failed" ? "error" : "connected");
181
+ }
182
+ catch (error) {
183
+ log("Pi agent_settled error", { error: String(error) });
184
+ finishStatus(ctx, status, "error");
185
+ }
186
+ });
187
+ pi.on("session_shutdown", async (_event, ctx) => {
188
+ clearStatus(ctx);
189
+ latestCtx = null;
190
+ captureState = createPiCaptureState();
191
+ promptsSinceProfileAnalysis = [];
192
+ globalThis[GLOBAL_PLUGIN_WARMUP_KEY] = false;
193
+ try {
194
+ await memoryClient.close();
195
+ }
196
+ catch {
197
+ // Idempotent cleanup: closing an already-closed client must never fail
198
+ // the shutdown flow across quit, reload, new, resume, and fork.
199
+ }
200
+ });
201
+ pi.registerTool({
202
+ name: "memory",
203
+ label: "Memory",
204
+ description: `Manage and query project memory (MATCH USER LANGUAGE: ${getLanguageName(CONFIG.autoCaptureLanguage || "en")}). Use 'search' with technical keywords/tags, 'add' to store knowledge, 'profile' for preferences. Use migrate/list-shards/export/import when a project directory moves. Search/list scope: project or all-projects.`,
205
+ parameters: Type.Object({
206
+ mode: Type.Optional(Type.Union(MEMORY_TOOL_MODES.map((mode) => Type.Literal(mode)))),
207
+ content: Type.Optional(Type.String()),
208
+ query: Type.Optional(Type.String()),
209
+ tags: Type.Optional(Type.String()),
210
+ type: Type.Optional(Type.String()),
211
+ memoryId: Type.Optional(Type.String()),
212
+ limit: Type.Optional(Type.Number()),
213
+ scope: Type.Optional(Type.Union([Type.Literal("project"), Type.Literal("all-projects")])),
214
+ fromPath: Type.Optional(Type.String()),
215
+ fromHash: Type.Optional(Type.String()),
216
+ outputPath: Type.Optional(Type.String()),
217
+ inputPath: Type.Optional(Type.String()),
218
+ dryRun: Type.Optional(Type.Boolean()),
219
+ allowLinkedSource: Type.Optional(Type.Boolean()),
220
+ }),
221
+ async execute(_toolCallId, args, _signal, _onUpdate, ctx) {
222
+ const result = await executeMemoryOperation(args, {
223
+ directory: ctx.cwd,
224
+ host: "pi",
225
+ hostSessionId: ctx.sessionManager.getSessionId(),
226
+ });
227
+ return {
228
+ content: [{ type: "text", text: JSON.stringify(result) }],
229
+ details: {},
230
+ };
231
+ },
232
+ });
233
+ registerPiHistoryImportCommand(pi, () => latestCtx);
234
+ }
@@ -0,0 +1,29 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type ImportPathMap } from "../../importer/importer.js";
3
+ /**
4
+ * Pi command surface for the historical-session importer:
5
+ *
6
+ * /memory-import-pi-history --dry-run
7
+ * /memory-import-pi-history
8
+ *
9
+ * Runs over the shared importer service. Extraction inherits the active Pi
10
+ * model (or the piProvider/piModel override). Source session files are only
11
+ * ever read.
12
+ */
13
+ export declare const PI_IMPORT_COMMAND = "memory-import-pi-history";
14
+ export declare const PI_IMPORT_USAGE = "Usage: /memory-import-pi-history [options]\n\nOptions:\n --dry-run Discover, map, and report; write nothing\n --force Reprocess units with terminal ledger states\n --scope=current-project Import only sessions from this project (default)\n --scope=all-projects Import every discovered session\n --session=<id-or-file> Import one exact session\n --since=<date> Only work units at/after this time\n --until=<date> Only work units at/before this time\n --max-sessions=<n> Limit discovery to the oldest n sessions\n --map=<oldPath>=<newPath> Remap a recorded cwd that no longer exists\n --root=<dir> Session root (default ~/.pi/agent/sessions)\n\nDates accept ISO 8601 (2026-01-01, 2026-01-01T10:00:00Z) or epoch\nmilliseconds. --since/--until filter on each work unit's user-entry\ntimestamp, inclusive. Unresolvable recorded cwds are skipped and listed\nin the report; use --map to import them into an existing directory.";
15
+ export interface ParsedImportArgs {
16
+ dryRun: boolean;
17
+ force: boolean;
18
+ scope: "current-project" | "all-projects";
19
+ session?: string;
20
+ since?: number;
21
+ until?: number;
22
+ maxSessions?: number;
23
+ root?: string;
24
+ pathMaps: ImportPathMap[];
25
+ help: boolean;
26
+ errors: string[];
27
+ }
28
+ export declare function parseImportArgs(raw: string): ParsedImportArgs;
29
+ export declare function registerPiHistoryImportCommand(pi: ExtensionAPI, getExtensionContext: () => ExtensionContext | null): void;
@@ -0,0 +1,232 @@
1
+ import { initConfigWithLegacyMigration, isConfigured } from "../../config.js";
2
+ import { log } from "../../services/logger.js";
3
+ import { memoryClient } from "../../services/client.js";
4
+ import { importPiHistory, } from "../../importer/importer.js";
5
+ import { loadPiSessionForImport } from "../../importer/session-loader.js";
6
+ import { createPiCaptureProvider, resolveModelFromContext } from "./provider.js";
7
+ /**
8
+ * Pi command surface for the historical-session importer:
9
+ *
10
+ * /memory-import-pi-history --dry-run
11
+ * /memory-import-pi-history
12
+ *
13
+ * Runs over the shared importer service. Extraction inherits the active Pi
14
+ * model (or the piProvider/piModel override). Source session files are only
15
+ * ever read.
16
+ */
17
+ export const PI_IMPORT_COMMAND = "memory-import-pi-history";
18
+ export const PI_IMPORT_USAGE = `Usage: /${PI_IMPORT_COMMAND} [options]
19
+
20
+ Options:
21
+ --dry-run Discover, map, and report; write nothing
22
+ --force Reprocess units with terminal ledger states
23
+ --scope=current-project Import only sessions from this project (default)
24
+ --scope=all-projects Import every discovered session
25
+ --session=<id-or-file> Import one exact session
26
+ --since=<date> Only work units at/after this time
27
+ --until=<date> Only work units at/before this time
28
+ --max-sessions=<n> Limit discovery to the oldest n sessions
29
+ --map=<oldPath>=<newPath> Remap a recorded cwd that no longer exists
30
+ --root=<dir> Session root (default ~/.pi/agent/sessions)
31
+
32
+ Dates accept ISO 8601 (2026-01-01, 2026-01-01T10:00:00Z) or epoch
33
+ milliseconds. --since/--until filter on each work unit's user-entry
34
+ timestamp, inclusive. Unresolvable recorded cwds are skipped and listed
35
+ in the report; use --map to import them into an existing directory.`;
36
+ function parseTimestamp(raw) {
37
+ if (!raw)
38
+ return undefined;
39
+ if (/^\d+$/.test(raw))
40
+ return Number(raw);
41
+ const parsed = Date.parse(raw);
42
+ return Number.isNaN(parsed) ? undefined : parsed;
43
+ }
44
+ export function parseImportArgs(raw) {
45
+ const result = {
46
+ dryRun: false,
47
+ force: false,
48
+ scope: "current-project",
49
+ pathMaps: [],
50
+ help: false,
51
+ errors: [],
52
+ };
53
+ for (const token of raw.trim().split(/\s+/).filter(Boolean)) {
54
+ if (token === "--help" || token === "-h") {
55
+ result.help = true;
56
+ continue;
57
+ }
58
+ if (token === "--dry-run") {
59
+ result.dryRun = true;
60
+ continue;
61
+ }
62
+ if (token === "--force") {
63
+ result.force = true;
64
+ continue;
65
+ }
66
+ const equals = token.indexOf("=");
67
+ const flag = equals === -1 ? token : token.slice(0, equals);
68
+ const value = equals === -1 ? "" : token.slice(equals + 1);
69
+ switch (flag) {
70
+ case "--scope":
71
+ if (value === "current-project" || value === "all-projects") {
72
+ result.scope = value;
73
+ }
74
+ else {
75
+ result.errors.push(`--scope must be current-project or all-projects, got "${value}"`);
76
+ }
77
+ break;
78
+ case "--session":
79
+ if (value)
80
+ result.session = value;
81
+ else
82
+ result.errors.push("--session requires a value");
83
+ break;
84
+ case "--since":
85
+ result.since = parseTimestamp(value);
86
+ if (result.since === undefined)
87
+ result.errors.push(`--since is not a valid date: "${value}"`);
88
+ break;
89
+ case "--until":
90
+ result.until = parseTimestamp(value);
91
+ if (result.until === undefined)
92
+ result.errors.push(`--until is not a valid date: "${value}"`);
93
+ break;
94
+ case "--max-sessions": {
95
+ const parsed = Number(value);
96
+ if (Number.isInteger(parsed) && parsed >= 0)
97
+ result.maxSessions = parsed;
98
+ else
99
+ result.errors.push(`--max-sessions must be a non-negative integer, got "${value}"`);
100
+ break;
101
+ }
102
+ case "--root":
103
+ if (value)
104
+ result.root = value;
105
+ else
106
+ result.errors.push("--root requires a value");
107
+ break;
108
+ case "--map": {
109
+ const separator = value.indexOf("=");
110
+ if (separator > 0 && separator < value.length - 1) {
111
+ result.pathMaps.push({
112
+ from: value.slice(0, separator),
113
+ to: value.slice(separator + 1),
114
+ });
115
+ }
116
+ else {
117
+ result.errors.push(`--map must be <oldPath>=<newPath>, got "${value}"`);
118
+ }
119
+ break;
120
+ }
121
+ default:
122
+ result.errors.push(`Unknown option: "${token}"`);
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ function formatReport(report) {
128
+ const lines = [
129
+ `Pi history import ${report.dryRun ? "(dry-run)" : ""}`.trim(),
130
+ ` sessions: ${report.sessionsLoaded} loaded, ${report.sessionsFilteredOut} filtered, ` +
131
+ `${report.sessionsUnrecognized} unrecognized, ` +
132
+ `${report.unresolvableSessions.length} unresolvable cwd`,
133
+ ` work units: ${report.unitsTotal} total, ` +
134
+ (report.dryRun
135
+ ? `${report.unitsWouldImport} would import`
136
+ : `${report.unitsImported} imported, ${report.unitsSkipped} skipped, ` +
137
+ `${report.unitsFailed} failed`) +
138
+ `, ${report.unitsAlreadyHandled} already handled`,
139
+ ];
140
+ for (const project of report.projects) {
141
+ lines.push(` project ${project.tag} (${project.sessions} sessions, ${project.units} units) -> ${project.directory}`);
142
+ }
143
+ if (report.loadErrors.length > 0) {
144
+ lines.push(` load errors: ${report.loadErrors.length}`);
145
+ }
146
+ return lines.join("\n");
147
+ }
148
+ let importCommandRunning = false;
149
+ export function registerPiHistoryImportCommand(pi, getExtensionContext) {
150
+ pi.registerCommand(PI_IMPORT_COMMAND, {
151
+ description: "Import historical Pi sessions into the shared memory store (try --dry-run first)",
152
+ handler: async (args, commandCtx) => {
153
+ const notify = (message) => {
154
+ if (getExtensionContext()?.hasUI) {
155
+ getExtensionContext()?.ui.notify(message, "info");
156
+ }
157
+ };
158
+ const parsed = parseImportArgs(args ?? "");
159
+ if (parsed.help) {
160
+ notify(PI_IMPORT_USAGE);
161
+ return;
162
+ }
163
+ if (parsed.errors.length > 0) {
164
+ notify(`memory-import-pi-history: ${parsed.errors.join("; ")}`);
165
+ return;
166
+ }
167
+ const ctx = getExtensionContext();
168
+ if (!ctx) {
169
+ notify("memory-import-pi-history: no active Pi session context; open a session first");
170
+ return;
171
+ }
172
+ if (!isConfigured()) {
173
+ notify("memory-import-pi-history: memory system not configured");
174
+ return;
175
+ }
176
+ if (importCommandRunning) {
177
+ notify("memory-import-pi-history: an import is already running");
178
+ return;
179
+ }
180
+ // Import against the current session's project and configuration.
181
+ initConfigWithLegacyMigration(ctx.cwd);
182
+ if (typeof commandCtx.waitForIdle === "function") {
183
+ await commandCtx.waitForIdle();
184
+ }
185
+ importCommandRunning = true;
186
+ try {
187
+ // Best-effort warmup so failures surface as per-unit errors instead of
188
+ // every unit failing on embedding initialisation.
189
+ try {
190
+ await memoryClient.warmup();
191
+ }
192
+ catch (error) {
193
+ log("Pi import: memory warmup failed", { error: String(error) });
194
+ }
195
+ const filters = {
196
+ scope: parsed.scope,
197
+ currentDirectory: ctx.cwd,
198
+ ...(parsed.session !== undefined ? { session: parsed.session } : {}),
199
+ ...(parsed.since !== undefined ? { since: parsed.since } : {}),
200
+ ...(parsed.until !== undefined ? { until: parsed.until } : {}),
201
+ ...(parsed.maxSessions !== undefined ? { maxSessions: parsed.maxSessions } : {}),
202
+ ...(parsed.root !== undefined ? { root: parsed.root } : {}),
203
+ ...(parsed.force ? { force: true } : {}),
204
+ ...(parsed.dryRun ? { dryRun: true } : {}),
205
+ pathMaps: parsed.pathMaps,
206
+ };
207
+ const provider = createPiCaptureProvider(() => resolveModelFromContext(ctx));
208
+ let lastProgressNotify = 0;
209
+ const report = await importPiHistory({
210
+ loadSession: loadPiSessionForImport,
211
+ provider,
212
+ onProgress: (processed, total, promptPreview) => {
213
+ if (processed - lastProgressNotify >= 25 || processed === total) {
214
+ lastProgressNotify = processed;
215
+ notify(`Pi import: ${processed}/${total} units (${promptPreview})`);
216
+ }
217
+ },
218
+ }, filters);
219
+ log("Pi history import report", { report });
220
+ notify(formatReport(report));
221
+ }
222
+ catch (error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ log("Pi history import failed", { error: message });
225
+ notify(`memory-import-pi-history failed: ${message}`);
226
+ }
227
+ finally {
228
+ importCommandRunning = false;
229
+ }
230
+ },
231
+ });
232
+ }
@@ -0,0 +1,39 @@
1
+ import type { UserProfileData } from "../../services/user-profile/types.js";
2
+ import type { PiModelHandle } from "./provider.js";
3
+ export interface ExistingProfileInput {
4
+ id: string;
5
+ profileData: string;
6
+ }
7
+ export interface ProfileAnalysisResult {
8
+ raw: UserProfileData;
9
+ merged: UserProfileData | null;
10
+ }
11
+ export interface PiProfileAnalyzer {
12
+ analyzeProfile(context: string, existingProfile: ExistingProfileInput | null): Promise<ProfileAnalysisResult | null>;
13
+ }
14
+ /**
15
+ * Bridge the shared user-profile analysis onto Pi's model runtime. Uses the
16
+ * same schema and system prompt as the OpenCode structured-output path, so
17
+ * analysis results are validated with the shared schema before they reach the
18
+ * profile storage layer. Model failures throw (mirroring provider semantics);
19
+ * a missing model returns null so callers can skip without disabling anything.
20
+ */
21
+ export declare function createPiProfileAnalyzer(resolveModel: () => PiModelHandle | null, timeoutMs?: number): PiProfileAnalyzer;
22
+ export interface PiProfileLearningInput {
23
+ directory: string;
24
+ prompts: string[];
25
+ resolveModel: () => PiModelHandle | null;
26
+ notify?: (notification: {
27
+ title: string;
28
+ message: string;
29
+ variant: "success" | "warning" | "error" | "info";
30
+ }) => Promise<void> | void;
31
+ }
32
+ /**
33
+ * Pi-side profile learning lifecycle: analyse a batch of settled user prompts
34
+ * through the model bridge and persist via the shared profile manager. Failures
35
+ * log (and optionally toast) without ever disabling memory operations. The
36
+ * decay/validation-task machinery of the OpenCode idle path is not ported;
37
+ * this applies the analysed batch directly (documented limitation).
38
+ */
39
+ export declare function performPiProfileLearning(input: PiProfileLearningInput): Promise<void>;
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ import { CONFIG } from "../../config.js";
3
+ import { createUserProfileAnalysisSchema, extractJsonObject } from "../../core/extraction.js";
4
+ import { log } from "../../services/logger.js";
5
+ import { getTags } from "../../services/tags.js";
6
+ /**
7
+ * System prompt mirrored from the OpenCode profile-learning path
8
+ * (src/services/user-memory-learning.ts, analyzeUserProfile). Kept local so
9
+ * the Pi adapter does not import an OpenCode-coupled module; update both when
10
+ * the analysis contract changes.
11
+ */
12
+ function buildProfileAnalysisSystemPrompt(existingProfile) {
13
+ return `You are a user behavior analyst for a coding assistant.
14
+
15
+ Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
16
+
17
+ CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
18
+
19
+ CRITICAL: All JSON string values MUST escape double quotes with backslash. Do NOT use unescaped quotation marks inside string values.
20
+
21
+ Respond with a single JSON object matching the update_user_profile contract.`;
22
+ }
23
+ function replyText(reply) {
24
+ return reply.content
25
+ .filter((block) => block.type === "text" && typeof block.text === "string")
26
+ .map((block) => block.text)
27
+ .join("\n")
28
+ .trim();
29
+ }
30
+ /**
31
+ * Bridge the shared user-profile analysis onto Pi's model runtime. Uses the
32
+ * same schema and system prompt as the OpenCode structured-output path, so
33
+ * analysis results are validated with the shared schema before they reach the
34
+ * profile storage layer. Model failures throw (mirroring provider semantics);
35
+ * a missing model returns null so callers can skip without disabling anything.
36
+ */
37
+ export function createPiProfileAnalyzer(resolveModel, timeoutMs = 120000) {
38
+ return {
39
+ async analyzeProfile(context, existingProfile) {
40
+ const model = resolveModel();
41
+ if (!model)
42
+ return null;
43
+ // Unref'd and cleared so the losing timeout timer never keeps the host
44
+ // process alive after the race settles.
45
+ let timeoutId;
46
+ try {
47
+ const reply = await Promise.race([
48
+ model.complete({
49
+ systemPrompt: buildProfileAnalysisSystemPrompt(Boolean(existingProfile)),
50
+ messages: [{ role: "user", content: context, timestamp: Date.now() }],
51
+ }),
52
+ new Promise((_, reject) => {
53
+ timeoutId = setTimeout(() => reject(new Error("pi profile analysis: timeout")), timeoutMs);
54
+ timeoutId?.unref?.();
55
+ }),
56
+ ]);
57
+ if (reply.stopReason === "error") {
58
+ throw new Error(`pi profile analysis: model call failed: ${reply.errorMessage || "unknown error"}`);
59
+ }
60
+ const schema = createUserProfileAnalysisSchema(z);
61
+ const parsed = schema.safeParse(extractJsonObject(replyText(reply)));
62
+ if (!parsed.success) {
63
+ log("pi profile analysis: model reply failed shared schema validation", {
64
+ provider: model.provider,
65
+ modelId: model.modelId,
66
+ });
67
+ throw new Error("pi profile analysis: invalid profile payload");
68
+ }
69
+ const rawData = parsed.data;
70
+ if (existingProfile) {
71
+ const { userProfileManager } = await import("../../services/user-profile/user-profile-manager.js");
72
+ const existingData = JSON.parse(existingProfile.profileData);
73
+ const merged = await userProfileManager.mergeProfileData(existingData, rawData, undefined, existingProfile.id);
74
+ return { raw: rawData, merged };
75
+ }
76
+ return { raw: rawData, merged: null };
77
+ }
78
+ finally {
79
+ if (timeoutId)
80
+ clearTimeout(timeoutId);
81
+ }
82
+ },
83
+ };
84
+ }
85
+ /**
86
+ * Pi-side profile learning lifecycle: analyse a batch of settled user prompts
87
+ * through the model bridge and persist via the shared profile manager. Failures
88
+ * log (and optionally toast) without ever disabling memory operations. The
89
+ * decay/validation-task machinery of the OpenCode idle path is not ported;
90
+ * this applies the analysed batch directly (documented limitation).
91
+ */
92
+ export async function performPiProfileLearning(input) {
93
+ if (input.prompts.length === 0)
94
+ return;
95
+ try {
96
+ const tags = getTags(input.directory);
97
+ if (!tags.user.userEmail) {
98
+ log("pi profile learning: skipped (no user email resolved)", {
99
+ directory: input.directory,
100
+ });
101
+ return;
102
+ }
103
+ const userId = tags.user.userEmail;
104
+ const { userProfileManager } = await import("../../services/user-profile/user-profile-manager.js");
105
+ const existing = await userProfileManager.getActiveProfile(userId);
106
+ const analyzer = createPiProfileAnalyzer(input.resolveModel);
107
+ const context = input.prompts.map((prompt, i) => `${i + 1}. ${prompt}`).join("\n");
108
+ const analysis = await analyzer.analyzeProfile(context, existing ? { id: existing.id, profileData: existing.profileData } : null);
109
+ if (!analysis) {
110
+ log("pi profile learning: skipped (no model available)", { userId });
111
+ return;
112
+ }
113
+ if (existing && analysis.merged) {
114
+ const updated = await userProfileManager.updateProfile(existing.id, analysis.merged, input.prompts.length, `Pi analysis of ${input.prompts.length} prompts`);
115
+ if (!updated) {
116
+ log("pi profile learning: update conflict", { profileId: existing.id });
117
+ return;
118
+ }
119
+ }
120
+ else {
121
+ await userProfileManager.createProfile(userId, tags.user.displayName || userId, tags.user.userName || userId, tags.user.userEmail || userId, analysis.raw, input.prompts.length);
122
+ }
123
+ log("pi profile learning: profile updated", {
124
+ userId,
125
+ prompts: input.prompts.length,
126
+ hadExisting: Boolean(existing),
127
+ });
128
+ if (CONFIG.showUserProfileToasts && input.notify) {
129
+ await Promise.resolve(input.notify({
130
+ title: "User Profile Updated",
131
+ message: `Analyzed ${input.prompts.length} prompts and updated your profile`,
132
+ variant: "success",
133
+ })).catch(() => { });
134
+ }
135
+ }
136
+ catch (error) {
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ log(`pi profile learning: aborted (${message})`);
139
+ if (CONFIG.showErrorToasts && input.notify) {
140
+ const shortReason = message.length > 100 ? message.substring(0, 100) + "..." : message;
141
+ await Promise.resolve(input.notify({
142
+ title: "Profile Learning Failed",
143
+ message: shortReason,
144
+ variant: "error",
145
+ })).catch(() => { });
146
+ }
147
+ }
148
+ }