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.
- package/LICENSE +21 -0
- package/README.md +587 -0
- package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
- package/dist/adapters/opencode/auto-capture-host.js +29 -0
- package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
- package/dist/adapters/opencode/auto-capture-summary.js +147 -0
- package/dist/adapters/opencode/conversation.d.ts +2 -0
- package/dist/adapters/opencode/conversation.js +50 -0
- package/dist/adapters/opencode/user-prompt.d.ts +9 -0
- package/dist/adapters/opencode/user-prompt.js +26 -0
- package/dist/adapters/pi/capture.d.ts +41 -0
- package/dist/adapters/pi/capture.js +91 -0
- package/dist/adapters/pi/conversation.d.ts +38 -0
- package/dist/adapters/pi/conversation.js +118 -0
- package/dist/adapters/pi/extension.d.ts +18 -0
- package/dist/adapters/pi/extension.js +234 -0
- package/dist/adapters/pi/import-command.d.ts +29 -0
- package/dist/adapters/pi/import-command.js +232 -0
- package/dist/adapters/pi/profile.d.ts +39 -0
- package/dist/adapters/pi/profile.js +148 -0
- package/dist/adapters/pi/provider.d.ts +50 -0
- package/dist/adapters/pi/provider.js +77 -0
- package/dist/config.d.ts +290 -0
- package/dist/config.js +742 -0
- package/dist/core/capture-context.d.ts +4 -0
- package/dist/core/capture-context.js +132 -0
- package/dist/core/capture.d.ts +15 -0
- package/dist/core/capture.js +68 -0
- package/dist/core/extraction.d.ts +80 -0
- package/dist/core/extraction.js +140 -0
- package/dist/core/host.d.ts +52 -0
- package/dist/core/host.js +1 -0
- package/dist/core/memory-operations.d.ts +27 -0
- package/dist/core/memory-operations.js +247 -0
- package/dist/core/retrieval.d.ts +12 -0
- package/dist/core/retrieval.js +72 -0
- package/dist/importer/discovery.d.ts +31 -0
- package/dist/importer/discovery.js +88 -0
- package/dist/importer/importer.d.ts +87 -0
- package/dist/importer/importer.js +260 -0
- package/dist/importer/ledger.d.ts +44 -0
- package/dist/importer/ledger.js +137 -0
- package/dist/importer/session-loader.d.ts +19 -0
- package/dist/importer/session-loader.js +12 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +601 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +11 -0
- package/dist/services/ai/ai-provider-factory.d.ts +7 -0
- package/dist/services/ai/ai-provider-factory.js +41 -0
- package/dist/services/ai/api-key-placeholder.d.ts +1 -0
- package/dist/services/ai/api-key-placeholder.js +14 -0
- package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
- package/dist/services/ai/internal-capture-sessions.js +33 -0
- package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
- package/dist/services/ai/opencode-diagnostics.js +36 -0
- package/dist/services/ai/opencode-host-config.d.ts +10 -0
- package/dist/services/ai/opencode-host-config.js +51 -0
- package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
- package/dist/services/ai/opencode-provider-loader.js +4 -0
- package/dist/services/ai/opencode-provider.d.ts +92 -0
- package/dist/services/ai/opencode-provider.js +419 -0
- package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
- package/dist/services/ai/opencode-sdk-client.js +38 -0
- package/dist/services/ai/profile-llm-client.d.ts +2 -0
- package/dist/services/ai/profile-llm-client.js +27 -0
- package/dist/services/ai/provider-config.d.ts +17 -0
- package/dist/services/ai/provider-config.js +30 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
- package/dist/services/ai/providers/anthropic-messages.js +220 -0
- package/dist/services/ai/providers/base-provider.d.ts +24 -0
- package/dist/services/ai/providers/base-provider.js +24 -0
- package/dist/services/ai/providers/google-gemini.d.ts +15 -0
- package/dist/services/ai/providers/google-gemini.js +228 -0
- package/dist/services/ai/providers/minimax.d.ts +35 -0
- package/dist/services/ai/providers/minimax.js +55 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
- package/dist/services/ai/providers/openai-chat-completion.js +387 -0
- package/dist/services/ai/providers/openai-responses.d.ts +13 -0
- package/dist/services/ai/providers/openai-responses.js +182 -0
- package/dist/services/ai/providers/orcarouter.d.ts +51 -0
- package/dist/services/ai/providers/orcarouter.js +66 -0
- package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
- package/dist/services/ai/session/ai-session-manager.js +211 -0
- package/dist/services/ai/session/session-types.d.ts +42 -0
- package/dist/services/ai/session/session-types.js +1 -0
- package/dist/services/ai/tools/tool-schema.d.ts +40 -0
- package/dist/services/ai/tools/tool-schema.js +24 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
- package/dist/services/ai/validators/user-profile-validator.js +114 -0
- package/dist/services/api-handlers.d.ts +186 -0
- package/dist/services/api-handlers.js +1284 -0
- package/dist/services/auth-token.d.ts +14 -0
- package/dist/services/auth-token.js +59 -0
- package/dist/services/auto-capture.d.ts +3 -0
- package/dist/services/auto-capture.js +127 -0
- package/dist/services/cleanup-service.d.ts +22 -0
- package/dist/services/cleanup-service.js +109 -0
- package/dist/services/client.d.ts +134 -0
- package/dist/services/client.js +327 -0
- package/dist/services/context.d.ts +10 -0
- package/dist/services/context.js +27 -0
- package/dist/services/cors.d.ts +12 -0
- package/dist/services/cors.js +52 -0
- package/dist/services/deduplication-service.d.ts +28 -0
- package/dist/services/deduplication-service.js +111 -0
- package/dist/services/embedding.d.ts +36 -0
- package/dist/services/embedding.js +200 -0
- package/dist/services/jsonc.d.ts +6 -0
- package/dist/services/jsonc.js +76 -0
- package/dist/services/language-detector.d.ts +2 -0
- package/dist/services/language-detector.js +33 -0
- package/dist/services/legacy-migration.d.ts +77 -0
- package/dist/services/legacy-migration.js +343 -0
- package/dist/services/logger.d.ts +1 -0
- package/dist/services/logger.js +119 -0
- package/dist/services/memory-portability-service.d.ts +37 -0
- package/dist/services/memory-portability-service.js +340 -0
- package/dist/services/memory-scope.d.ts +16 -0
- package/dist/services/memory-scope.js +43 -0
- package/dist/services/migration-service.d.ts +42 -0
- package/dist/services/migration-service.js +307 -0
- package/dist/services/onnxruntime-resolve.d.ts +24 -0
- package/dist/services/onnxruntime-resolve.js +174 -0
- package/dist/services/privacy.d.ts +26 -0
- package/dist/services/privacy.js +69 -0
- package/dist/services/request-timeouts.d.ts +9 -0
- package/dist/services/request-timeouts.js +11 -0
- package/dist/services/runtime-require.d.ts +17 -0
- package/dist/services/runtime-require.js +75 -0
- package/dist/services/secret-resolver.d.ts +1 -0
- package/dist/services/secret-resolver.js +65 -0
- package/dist/services/shard-inventory-service.d.ts +69 -0
- package/dist/services/shard-inventory-service.js +315 -0
- package/dist/services/shard-path-migration-service.d.ts +31 -0
- package/dist/services/shard-path-migration-service.js +367 -0
- package/dist/services/tag-prefix-migration.d.ts +60 -0
- package/dist/services/tag-prefix-migration.js +439 -0
- package/dist/services/tag-prefix-scan.d.ts +26 -0
- package/dist/services/tag-prefix-scan.js +91 -0
- package/dist/services/tags.d.ts +29 -0
- package/dist/services/tags.js +251 -0
- package/dist/services/turso/connection-manager.d.ts +15 -0
- package/dist/services/turso/connection-manager.js +163 -0
- package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
- package/dist/services/turso/cross-process-write-lock.js +89 -0
- package/dist/services/turso/legacy-migrator.d.ts +1 -0
- package/dist/services/turso/legacy-migrator.js +540 -0
- package/dist/services/turso/lifecycle.d.ts +1 -0
- package/dist/services/turso/lifecycle.js +14 -0
- package/dist/services/turso/operation-lock.d.ts +2 -0
- package/dist/services/turso/operation-lock.js +61 -0
- package/dist/services/turso/ready.d.ts +2 -0
- package/dist/services/turso/ready.js +36 -0
- package/dist/services/turso/shard-manager.d.ts +38 -0
- package/dist/services/turso/shard-manager.js +496 -0
- package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
- package/dist/services/turso/sqlite-handle-release.js +56 -0
- package/dist/services/turso/turso-db.d.ts +18 -0
- package/dist/services/turso/turso-db.js +53 -0
- package/dist/services/turso/types.d.ts +42 -0
- package/dist/services/turso/types.js +1 -0
- package/dist/services/turso/vector-search.d.ts +41 -0
- package/dist/services/turso/vector-search.js +384 -0
- package/dist/services/turso/vector-utils.d.ts +7 -0
- package/dist/services/turso/vector-utils.js +67 -0
- package/dist/services/user-memory-learning.d.ts +97 -0
- package/dist/services/user-memory-learning.js +673 -0
- package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
- package/dist/services/user-profile/ai-cleanup.js +486 -0
- package/dist/services/user-profile/profile-context.d.ts +1 -0
- package/dist/services/user-profile/profile-context.js +105 -0
- package/dist/services/user-profile/profile-utils.d.ts +2 -0
- package/dist/services/user-profile/profile-utils.js +45 -0
- package/dist/services/user-profile/types.d.ts +92 -0
- package/dist/services/user-profile/types.js +1 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
- package/dist/services/user-profile/user-profile-manager.js +1660 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
- package/dist/services/user-prompt/user-prompt-manager.js +303 -0
- package/dist/services/web-api-auth.d.ts +3 -0
- package/dist/services/web-api-auth.js +25 -0
- package/dist/services/web-auth.d.ts +46 -0
- package/dist/services/web-auth.js +105 -0
- package/dist/services/web-server.d.ts +57 -0
- package/dist/services/web-server.js +659 -0
- package/dist/shared/api/index.d.ts +4 -0
- package/dist/shared/api/index.js +2 -0
- package/dist/shared/api/portability-schemas.d.ts +56 -0
- package/dist/shared/api/portability-schemas.js +39 -0
- package/dist/shared/api/schemas.d.ts +295 -0
- package/dist/shared/api/schemas.js +134 -0
- package/dist/shared/api/types.d.ts +28 -0
- package/dist/shared/api/types.js +1 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/context-limit.d.ts +10 -0
- package/dist/utils/context-limit.js +43 -0
- package/dist/utils/math.d.ts +7 -0
- package/dist/utils/math.js +48 -0
- package/dist/utils/profile.d.ts +2 -0
- package/dist/utils/profile.js +15 -0
- package/dist/v2/adapter.d.ts +5 -0
- package/dist/v2/adapter.js +218 -0
- package/dist/v2/legacy-client.d.ts +15 -0
- package/dist/v2/legacy-client.js +257 -0
- package/dist/v2/memory-bridge.d.ts +18 -0
- package/dist/v2/memory-bridge.js +29 -0
- package/dist/v2/plugin.d.ts +3 -0
- package/dist/v2/plugin.js +24 -0
- package/dist/web/assets/index-DPjEpD1h.css +2 -0
- package/dist/web/assets/index-do26c4JJ.js +122 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/omms-icon.png +0 -0
- package/package.json +138 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface UserPrompt {
|
|
2
|
+
id: string;
|
|
3
|
+
sessionId: string;
|
|
4
|
+
messageId: string;
|
|
5
|
+
projectPath: string | null;
|
|
6
|
+
content: string;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
captured: boolean;
|
|
9
|
+
userLearningCaptured: boolean;
|
|
10
|
+
linkedMemoryId: string | null;
|
|
11
|
+
capture_attempts: number;
|
|
12
|
+
providerId: string | null;
|
|
13
|
+
modelId: string | null;
|
|
14
|
+
}
|
|
15
|
+
export declare class UserPromptManager {
|
|
16
|
+
private db;
|
|
17
|
+
private dbPath;
|
|
18
|
+
private initPromise;
|
|
19
|
+
constructor();
|
|
20
|
+
reset(): void;
|
|
21
|
+
private initialize;
|
|
22
|
+
private ready;
|
|
23
|
+
private initDatabase;
|
|
24
|
+
savePrompt(sessionId: string, messageId: string, projectPath: string, content: string): Promise<string>;
|
|
25
|
+
setPromptModel(messageId: string, providerId: string, modelId: string): Promise<void>;
|
|
26
|
+
getLastUncapturedPrompt(sessionId: string): Promise<UserPrompt | null>;
|
|
27
|
+
getUncapturedPromptsForSession(sessionId: string): Promise<UserPrompt[]>;
|
|
28
|
+
deletePrompt(promptId: string): Promise<void>;
|
|
29
|
+
markAsCaptured(promptId: string): Promise<void>;
|
|
30
|
+
claimPrompt(promptId: string): Promise<boolean>;
|
|
31
|
+
recordFailedAttempt(promptId: string): Promise<void>;
|
|
32
|
+
releaseClaim(promptId: string): Promise<boolean>;
|
|
33
|
+
countUncapturedPrompts(): Promise<number>;
|
|
34
|
+
getUncapturedPrompts(limit: number): Promise<UserPrompt[]>;
|
|
35
|
+
markMultipleAsCaptured(promptIds: string[]): Promise<void>;
|
|
36
|
+
countUnanalyzedForUserLearning(): Promise<number>;
|
|
37
|
+
getPromptsForUserLearning(limit: number): Promise<UserPrompt[]>;
|
|
38
|
+
markAsUserLearningCaptured(promptId: string): Promise<void>;
|
|
39
|
+
markMultipleAsUserLearningCaptured(promptIds: string[]): Promise<void>;
|
|
40
|
+
deleteOldPrompts(cutoffTime: number): Promise<{
|
|
41
|
+
deleted: number;
|
|
42
|
+
linkedMemoryIds: string[];
|
|
43
|
+
}>;
|
|
44
|
+
vacuum(): Promise<void>;
|
|
45
|
+
linkMemoryToPrompt(promptId: string, memoryId: string): Promise<void>;
|
|
46
|
+
getPromptById(promptId: string): Promise<UserPrompt | null>;
|
|
47
|
+
getCapturedPrompts(projectPath?: string): Promise<UserPrompt[]>;
|
|
48
|
+
searchPrompts(query: string, projectPath?: string, limit?: number): Promise<UserPrompt[]>;
|
|
49
|
+
getPromptsByIds(ids: string[]): Promise<UserPrompt[]>;
|
|
50
|
+
updateProjectPath(oldProjectPath: string, newProjectPath: string): Promise<number>;
|
|
51
|
+
private rowToPrompt;
|
|
52
|
+
}
|
|
53
|
+
export declare const userPromptManager: UserPromptManager;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { tursoConnectionManager } from "../turso/connection-manager.js";
|
|
3
|
+
import { CONFIG } from "../../config.js";
|
|
4
|
+
const USER_PROMPTS_DB_NAME = "user-prompts.db";
|
|
5
|
+
export class UserPromptManager {
|
|
6
|
+
db = null;
|
|
7
|
+
dbPath;
|
|
8
|
+
initPromise = null;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.dbPath = join(CONFIG.storagePath, USER_PROMPTS_DB_NAME);
|
|
11
|
+
}
|
|
12
|
+
reset() {
|
|
13
|
+
this.db = null;
|
|
14
|
+
this.initPromise = null;
|
|
15
|
+
this.dbPath = join(CONFIG.storagePath, USER_PROMPTS_DB_NAME);
|
|
16
|
+
}
|
|
17
|
+
async initialize() {
|
|
18
|
+
if (this.initPromise) {
|
|
19
|
+
return this.initPromise;
|
|
20
|
+
}
|
|
21
|
+
this.initPromise = (async () => {
|
|
22
|
+
try {
|
|
23
|
+
this.dbPath = join(CONFIG.storagePath, USER_PROMPTS_DB_NAME);
|
|
24
|
+
this.db = await tursoConnectionManager.getConnection(this.dbPath);
|
|
25
|
+
await this.initDatabase();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
this.initPromise = null;
|
|
29
|
+
this.db = null;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
return this.initPromise;
|
|
34
|
+
}
|
|
35
|
+
async ready() {
|
|
36
|
+
if (!this.db || !this.initPromise) {
|
|
37
|
+
await this.initialize();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
await this.initPromise;
|
|
41
|
+
}
|
|
42
|
+
if (!this.db) {
|
|
43
|
+
throw new Error("UserPromptManager: database not initialized");
|
|
44
|
+
}
|
|
45
|
+
return this.db;
|
|
46
|
+
}
|
|
47
|
+
async initDatabase() {
|
|
48
|
+
const db = this.db;
|
|
49
|
+
await db.batch([
|
|
50
|
+
{
|
|
51
|
+
sql: `
|
|
52
|
+
CREATE TABLE IF NOT EXISTS user_prompts (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
session_id TEXT NOT NULL,
|
|
55
|
+
message_id TEXT NOT NULL,
|
|
56
|
+
project_path TEXT,
|
|
57
|
+
content TEXT NOT NULL,
|
|
58
|
+
created_at INTEGER NOT NULL,
|
|
59
|
+
captured INTEGER DEFAULT 0,
|
|
60
|
+
user_learning_captured BOOLEAN DEFAULT 0,
|
|
61
|
+
linked_memory_id TEXT,
|
|
62
|
+
capture_attempts INTEGER DEFAULT 0,
|
|
63
|
+
provider_id TEXT,
|
|
64
|
+
model_id TEXT
|
|
65
|
+
)
|
|
66
|
+
`,
|
|
67
|
+
},
|
|
68
|
+
{ sql: "UPDATE user_prompts SET captured = 0 WHERE captured = 2" },
|
|
69
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_user_prompts_session ON user_prompts(session_id)" },
|
|
70
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_user_prompts_captured ON user_prompts(captured)" },
|
|
71
|
+
{
|
|
72
|
+
sql: "CREATE INDEX IF NOT EXISTS idx_user_prompts_created ON user_prompts(created_at DESC)",
|
|
73
|
+
},
|
|
74
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_user_prompts_project ON user_prompts(project_path)" },
|
|
75
|
+
{
|
|
76
|
+
sql: "CREATE INDEX IF NOT EXISTS idx_user_prompts_linked ON user_prompts(linked_memory_id)",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
sql: "CREATE INDEX IF NOT EXISTS idx_user_prompts_user_learning ON user_prompts(user_learning_captured)",
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
for (const column of [
|
|
83
|
+
"capture_attempts INTEGER DEFAULT 0",
|
|
84
|
+
"provider_id TEXT",
|
|
85
|
+
"model_id TEXT",
|
|
86
|
+
]) {
|
|
87
|
+
try {
|
|
88
|
+
await db.run(`ALTER TABLE user_prompts ADD COLUMN ${column}`);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!String(error?.message ?? error).includes("duplicate column")) {
|
|
92
|
+
console.warn(`Failed to add ${column.split(" ")[0]} column:`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async savePrompt(sessionId, messageId, projectPath, content) {
|
|
98
|
+
const db = await this.ready();
|
|
99
|
+
const id = `prompt_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const changes = await db.run(`
|
|
102
|
+
INSERT INTO user_prompts (id, session_id, message_id, project_path, content, created_at, captured)
|
|
103
|
+
SELECT ?, ?, ?, ?, ?, ?, 0
|
|
104
|
+
WHERE NOT EXISTS (
|
|
105
|
+
SELECT 1 FROM user_prompts WHERE session_id = ? AND message_id = ?
|
|
106
|
+
)
|
|
107
|
+
`, [id, sessionId, messageId, projectPath, content, now, sessionId, messageId]);
|
|
108
|
+
if (changes > 0)
|
|
109
|
+
return id;
|
|
110
|
+
const existing = await db.get(`SELECT id FROM user_prompts
|
|
111
|
+
WHERE session_id = ? AND message_id = ?
|
|
112
|
+
ORDER BY created_at ASC
|
|
113
|
+
LIMIT 1`, [sessionId, messageId]);
|
|
114
|
+
if (existing?.id)
|
|
115
|
+
return String(existing.id);
|
|
116
|
+
throw new Error("Failed to save or locate user prompt");
|
|
117
|
+
}
|
|
118
|
+
async setPromptModel(messageId, providerId, modelId) {
|
|
119
|
+
const db = await this.ready();
|
|
120
|
+
await db.run(`UPDATE user_prompts SET provider_id = ?, model_id = ? WHERE message_id = ?`, [
|
|
121
|
+
providerId,
|
|
122
|
+
modelId,
|
|
123
|
+
messageId,
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
async getLastUncapturedPrompt(sessionId) {
|
|
127
|
+
const db = await this.ready();
|
|
128
|
+
const maxRetries = CONFIG.autoCaptureMaxRetries ?? 3;
|
|
129
|
+
const row = await db.get(`
|
|
130
|
+
SELECT * FROM user_prompts
|
|
131
|
+
WHERE session_id = ? AND captured = 0 AND capture_attempts < ?
|
|
132
|
+
ORDER BY created_at DESC
|
|
133
|
+
LIMIT 1
|
|
134
|
+
`, [sessionId, maxRetries]);
|
|
135
|
+
return row ? this.rowToPrompt(row) : null;
|
|
136
|
+
}
|
|
137
|
+
async getUncapturedPromptsForSession(sessionId) {
|
|
138
|
+
const db = await this.ready();
|
|
139
|
+
const maxRetries = CONFIG.autoCaptureMaxRetries ?? 3;
|
|
140
|
+
const rows = await db.all(`
|
|
141
|
+
SELECT * FROM user_prompts
|
|
142
|
+
WHERE session_id = ? AND captured = 0 AND capture_attempts < ?
|
|
143
|
+
ORDER BY created_at ASC
|
|
144
|
+
`, [sessionId, maxRetries]);
|
|
145
|
+
return rows.map((row) => this.rowToPrompt(row));
|
|
146
|
+
}
|
|
147
|
+
async deletePrompt(promptId) {
|
|
148
|
+
const db = await this.ready();
|
|
149
|
+
await db.run(`DELETE FROM user_prompts WHERE id = ?`, [promptId]);
|
|
150
|
+
}
|
|
151
|
+
async markAsCaptured(promptId) {
|
|
152
|
+
const db = await this.ready();
|
|
153
|
+
await db.run(`UPDATE user_prompts SET captured = 1 WHERE id = ?`, [promptId]);
|
|
154
|
+
}
|
|
155
|
+
async claimPrompt(promptId) {
|
|
156
|
+
const db = await this.ready();
|
|
157
|
+
const changes = await db.run(`UPDATE user_prompts SET captured = 2 WHERE id = ? AND captured = 0`, [promptId]);
|
|
158
|
+
return changes > 0;
|
|
159
|
+
}
|
|
160
|
+
async recordFailedAttempt(promptId) {
|
|
161
|
+
const db = await this.ready();
|
|
162
|
+
await db.run(`UPDATE user_prompts SET capture_attempts = capture_attempts + 1 WHERE id = ?`, [
|
|
163
|
+
promptId,
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
async releaseClaim(promptId) {
|
|
167
|
+
const db = await this.ready();
|
|
168
|
+
const changes = await db.run(`UPDATE user_prompts SET captured = 0 WHERE id = ? AND captured = 2`, [promptId]);
|
|
169
|
+
return changes > 0;
|
|
170
|
+
}
|
|
171
|
+
async countUncapturedPrompts() {
|
|
172
|
+
const db = await this.ready();
|
|
173
|
+
const maxRetries = CONFIG.autoCaptureMaxRetries ?? 3;
|
|
174
|
+
const row = await db.get(`SELECT COUNT(*) as count FROM user_prompts WHERE captured = 0 AND capture_attempts < ?`, [maxRetries]);
|
|
175
|
+
return Number(row?.count ?? 0);
|
|
176
|
+
}
|
|
177
|
+
async getUncapturedPrompts(limit) {
|
|
178
|
+
const db = await this.ready();
|
|
179
|
+
const maxRetries = CONFIG.autoCaptureMaxRetries ?? 3;
|
|
180
|
+
const rows = await db.all(`
|
|
181
|
+
SELECT * FROM user_prompts
|
|
182
|
+
WHERE captured = 0 AND capture_attempts < ?
|
|
183
|
+
ORDER BY capture_attempts ASC, created_at ASC
|
|
184
|
+
LIMIT ?
|
|
185
|
+
`, [maxRetries, limit]);
|
|
186
|
+
return rows.map((row) => this.rowToPrompt(row));
|
|
187
|
+
}
|
|
188
|
+
async markMultipleAsCaptured(promptIds) {
|
|
189
|
+
if (promptIds.length === 0)
|
|
190
|
+
return;
|
|
191
|
+
const db = await this.ready();
|
|
192
|
+
const placeholders = promptIds.map(() => "?").join(",");
|
|
193
|
+
await db.run(`UPDATE user_prompts SET captured = 1 WHERE id IN (${placeholders})`, promptIds);
|
|
194
|
+
}
|
|
195
|
+
async countUnanalyzedForUserLearning() {
|
|
196
|
+
const db = await this.ready();
|
|
197
|
+
const row = await db.get(`SELECT COUNT(*) as count FROM user_prompts WHERE user_learning_captured = 0`);
|
|
198
|
+
return Number(row?.count ?? 0);
|
|
199
|
+
}
|
|
200
|
+
async getPromptsForUserLearning(limit) {
|
|
201
|
+
const db = await this.ready();
|
|
202
|
+
const rows = await db.all(`
|
|
203
|
+
SELECT * FROM user_prompts
|
|
204
|
+
WHERE user_learning_captured = 0
|
|
205
|
+
ORDER BY created_at ASC
|
|
206
|
+
LIMIT ?
|
|
207
|
+
`, [limit]);
|
|
208
|
+
return rows.map((row) => this.rowToPrompt(row));
|
|
209
|
+
}
|
|
210
|
+
async markAsUserLearningCaptured(promptId) {
|
|
211
|
+
const db = await this.ready();
|
|
212
|
+
await db.run(`UPDATE user_prompts SET user_learning_captured = 1 WHERE id = ?`, [promptId]);
|
|
213
|
+
}
|
|
214
|
+
async markMultipleAsUserLearningCaptured(promptIds) {
|
|
215
|
+
if (promptIds.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
const db = await this.ready();
|
|
218
|
+
const placeholders = promptIds.map(() => "?").join(",");
|
|
219
|
+
await db.run(`UPDATE user_prompts SET user_learning_captured = 1 WHERE id IN (${placeholders})`, promptIds);
|
|
220
|
+
}
|
|
221
|
+
async deleteOldPrompts(cutoffTime) {
|
|
222
|
+
const db = await this.ready();
|
|
223
|
+
const linkedRows = await db.all(`
|
|
224
|
+
SELECT linked_memory_id FROM user_prompts
|
|
225
|
+
WHERE created_at < ? AND linked_memory_id IS NOT NULL
|
|
226
|
+
`, [cutoffTime]);
|
|
227
|
+
const linkedMemoryIds = linkedRows
|
|
228
|
+
.map((row) => row.linked_memory_id)
|
|
229
|
+
.filter((id) => Boolean(id));
|
|
230
|
+
const deleted = await db.run(`DELETE FROM user_prompts WHERE created_at < ?`, [cutoffTime]);
|
|
231
|
+
return { deleted, linkedMemoryIds };
|
|
232
|
+
}
|
|
233
|
+
async vacuum() {
|
|
234
|
+
const db = await this.ready();
|
|
235
|
+
await db.run("VACUUM");
|
|
236
|
+
}
|
|
237
|
+
async linkMemoryToPrompt(promptId, memoryId) {
|
|
238
|
+
const db = await this.ready();
|
|
239
|
+
await db.run(`UPDATE user_prompts SET linked_memory_id = ? WHERE id = ?`, [memoryId, promptId]);
|
|
240
|
+
}
|
|
241
|
+
async getPromptById(promptId) {
|
|
242
|
+
const db = await this.ready();
|
|
243
|
+
const row = await db.get(`SELECT * FROM user_prompts WHERE id = ?`, [promptId]);
|
|
244
|
+
return row ? this.rowToPrompt(row) : null;
|
|
245
|
+
}
|
|
246
|
+
async getCapturedPrompts(projectPath) {
|
|
247
|
+
const db = await this.ready();
|
|
248
|
+
const rows = projectPath
|
|
249
|
+
? await db.all(`SELECT * FROM user_prompts
|
|
250
|
+
WHERE captured = 1 AND REPLACE(project_path, '\\', '/') = ?
|
|
251
|
+
ORDER BY created_at DESC`, [projectPath.replace(/\\/g, "/")])
|
|
252
|
+
: await db.all(`SELECT * FROM user_prompts WHERE captured = 1 ORDER BY created_at DESC`);
|
|
253
|
+
return rows.map((row) => this.rowToPrompt(row));
|
|
254
|
+
}
|
|
255
|
+
async searchPrompts(query, projectPath, limit = 20) {
|
|
256
|
+
const db = await this.ready();
|
|
257
|
+
const params = [`%${query}%`];
|
|
258
|
+
let sql = `SELECT * FROM user_prompts WHERE content LIKE ? AND captured = 1`;
|
|
259
|
+
if (projectPath) {
|
|
260
|
+
sql += ` AND REPLACE(project_path, '\\', '/') = ?`;
|
|
261
|
+
params.push(projectPath.replace(/\\/g, "/"));
|
|
262
|
+
}
|
|
263
|
+
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
264
|
+
params.push(limit);
|
|
265
|
+
const rows = await db.all(sql, params);
|
|
266
|
+
return rows.map((row) => this.rowToPrompt(row));
|
|
267
|
+
}
|
|
268
|
+
async getPromptsByIds(ids) {
|
|
269
|
+
if (ids.length === 0)
|
|
270
|
+
return [];
|
|
271
|
+
const db = await this.ready();
|
|
272
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
273
|
+
const rows = await db.all(`SELECT * FROM user_prompts WHERE id IN (${placeholders})`, ids);
|
|
274
|
+
return rows.map((row) => this.rowToPrompt(row));
|
|
275
|
+
}
|
|
276
|
+
async updateProjectPath(oldProjectPath, newProjectPath) {
|
|
277
|
+
if (!oldProjectPath || !newProjectPath || oldProjectPath === newProjectPath) {
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
const db = await this.ready();
|
|
281
|
+
const normalizedOldPath = oldProjectPath.replace(/\\/g, "/");
|
|
282
|
+
return db.run(`UPDATE user_prompts
|
|
283
|
+
SET project_path = ?
|
|
284
|
+
WHERE REPLACE(project_path, '\\', '/') = ?`, [newProjectPath, normalizedOldPath]);
|
|
285
|
+
}
|
|
286
|
+
rowToPrompt(row) {
|
|
287
|
+
return {
|
|
288
|
+
id: String(row.id),
|
|
289
|
+
sessionId: String(row.session_id),
|
|
290
|
+
messageId: String(row.message_id),
|
|
291
|
+
projectPath: row.project_path ? String(row.project_path) : null,
|
|
292
|
+
content: String(row.content),
|
|
293
|
+
createdAt: Number(row.created_at),
|
|
294
|
+
captured: Number(row.captured) === 1,
|
|
295
|
+
userLearningCaptured: Number(row.user_learning_captured) === 1,
|
|
296
|
+
linkedMemoryId: row.linked_memory_id ? String(row.linked_memory_id) : null,
|
|
297
|
+
capture_attempts: Number(row.capture_attempts ?? 0),
|
|
298
|
+
providerId: row.provider_id ? String(row.provider_id) : null,
|
|
299
|
+
modelId: row.model_id ? String(row.model_id) : null,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export const userPromptManager = new UserPromptManager();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
2
|
+
export declare function assertWebServerNetworkAuth(host: string, apiToken?: string, basicAuthEnabled?: boolean): void;
|
|
3
|
+
export declare function authorizeApiRequest(req: Request, apiToken?: string): Response | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getRequestToken } from "./auth-token.js";
|
|
2
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0:0:0:0:0:0:0:1"]);
|
|
3
|
+
export function isLoopbackHost(host) {
|
|
4
|
+
return LOOPBACK_HOSTS.has(host.trim().toLowerCase());
|
|
5
|
+
}
|
|
6
|
+
export function assertWebServerNetworkAuth(host, apiToken, basicAuthEnabled = false) {
|
|
7
|
+
if (!isLoopbackHost(host) && !apiToken && !basicAuthEnabled) {
|
|
8
|
+
throw new Error(`webServerHost "${host}" exposes the API on the network. Set webServerApiToken in omms.jsonc, or bind to 127.0.0.1.`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function authorizeApiRequest(req, apiToken) {
|
|
12
|
+
if (!apiToken)
|
|
13
|
+
return null;
|
|
14
|
+
const header = req.headers.get("authorization");
|
|
15
|
+
const bearer = header?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
|
|
16
|
+
const alt = getRequestToken(req);
|
|
17
|
+
const token = bearer || alt;
|
|
18
|
+
if (token && token === apiToken) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), {
|
|
22
|
+
status: 401,
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const WEB_AUTH_REALM = "OpenCode Memory Explorer";
|
|
2
|
+
export interface WebAuthOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Plain-text password used for HTTP Basic Auth. Empty / undefined disables
|
|
5
|
+
* auth entirely (the previous default). The caller is responsible for
|
|
6
|
+
* resolving `env://` / `file://` shortcuts via the shared
|
|
7
|
+
* `resolveSecretValue` helper before passing the value in.
|
|
8
|
+
*/
|
|
9
|
+
password?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Plain-text username. Defaults to `os.userInfo().username` so that, when
|
|
12
|
+
* the server is bound to a LAN-reachable interface, the obvious choice
|
|
13
|
+
* is the OS account that launched opencode.
|
|
14
|
+
*/
|
|
15
|
+
username?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WebAuthConfig {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
username: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AuthCheckResult {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
response?: Response;
|
|
24
|
+
}
|
|
25
|
+
export declare class WebAuth {
|
|
26
|
+
private readonly enabled;
|
|
27
|
+
private readonly username;
|
|
28
|
+
private readonly expectedUsername;
|
|
29
|
+
private readonly expectedPassword;
|
|
30
|
+
constructor(options?: WebAuthOptions);
|
|
31
|
+
getConfig(): WebAuthConfig;
|
|
32
|
+
isEnabled(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Validate the HTTP Basic Auth credentials on the incoming request.
|
|
35
|
+
*
|
|
36
|
+
* Returns `{ ok: true }` when auth is disabled, when the request is for a
|
|
37
|
+
* path the user-facing browser must reach without credentials (the health
|
|
38
|
+
* probe), or when the supplied `Authorization: Basic …` header matches the
|
|
39
|
+
* configured username/password.
|
|
40
|
+
*
|
|
41
|
+
* Otherwise returns a fully-formed 401 Response carrying the
|
|
42
|
+
* `WWW-Authenticate` challenge so browsers pop their native login dialog.
|
|
43
|
+
*/
|
|
44
|
+
check(req: Request, path: string): AuthCheckResult;
|
|
45
|
+
challenge(): Response;
|
|
46
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { userInfo } from "node:os";
|
|
3
|
+
export const WEB_AUTH_REALM = "OpenCode Memory Explorer";
|
|
4
|
+
export class WebAuth {
|
|
5
|
+
enabled;
|
|
6
|
+
username;
|
|
7
|
+
expectedUsername;
|
|
8
|
+
expectedPassword;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
const password = (options.password ?? "").trim();
|
|
11
|
+
this.enabled = password.length > 0;
|
|
12
|
+
const explicitUsername = (options.username ?? "").trim();
|
|
13
|
+
this.username = explicitUsername || safeOsUsername();
|
|
14
|
+
this.expectedUsername = Buffer.from(this.username, "utf8");
|
|
15
|
+
this.expectedPassword = Buffer.from(password, "utf8");
|
|
16
|
+
}
|
|
17
|
+
getConfig() {
|
|
18
|
+
return { enabled: this.enabled, username: this.username };
|
|
19
|
+
}
|
|
20
|
+
isEnabled() {
|
|
21
|
+
return this.enabled;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate the HTTP Basic Auth credentials on the incoming request.
|
|
25
|
+
*
|
|
26
|
+
* Returns `{ ok: true }` when auth is disabled, when the request is for a
|
|
27
|
+
* path the user-facing browser must reach without credentials (the health
|
|
28
|
+
* probe), or when the supplied `Authorization: Basic …` header matches the
|
|
29
|
+
* configured username/password.
|
|
30
|
+
*
|
|
31
|
+
* Otherwise returns a fully-formed 401 Response carrying the
|
|
32
|
+
* `WWW-Authenticate` challenge so browsers pop their native login dialog.
|
|
33
|
+
*/
|
|
34
|
+
check(req, path) {
|
|
35
|
+
if (!this.enabled)
|
|
36
|
+
return { ok: true };
|
|
37
|
+
if (path === "/api/health")
|
|
38
|
+
return { ok: true };
|
|
39
|
+
const header = req.headers.get("Authorization");
|
|
40
|
+
if (header) {
|
|
41
|
+
const decoded = decodeBasicAuth(header);
|
|
42
|
+
if (decoded &&
|
|
43
|
+
constantTimeEquals(decoded.username, this.expectedUsername) &&
|
|
44
|
+
constantTimeEquals(decoded.password, this.expectedPassword)) {
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { ok: false, response: this.challenge() };
|
|
49
|
+
}
|
|
50
|
+
challenge() {
|
|
51
|
+
return new Response("Authentication required", {
|
|
52
|
+
status: 401,
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
55
|
+
"WWW-Authenticate": `Basic realm="${WEB_AUTH_REALM}", charset="UTF-8"`,
|
|
56
|
+
"Cache-Control": "no-store",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function safeOsUsername() {
|
|
62
|
+
try {
|
|
63
|
+
const info = userInfo();
|
|
64
|
+
if (info.username && info.username.length > 0)
|
|
65
|
+
return info.username;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// userInfo can throw on some sandboxes; fall through to env fallbacks.
|
|
69
|
+
}
|
|
70
|
+
if (process.env.USER && process.env.USER.length > 0)
|
|
71
|
+
return process.env.USER;
|
|
72
|
+
if (process.env.USERNAME && process.env.USERNAME.length > 0)
|
|
73
|
+
return process.env.USERNAME;
|
|
74
|
+
return "user";
|
|
75
|
+
}
|
|
76
|
+
function constantTimeEquals(provided, expected) {
|
|
77
|
+
const providedBuf = Buffer.from(provided ?? "", "utf8");
|
|
78
|
+
if (providedBuf.length !== expected.length) {
|
|
79
|
+
// Run a dummy compare so the call duration stays independent of length.
|
|
80
|
+
timingSafeEqual(expected, expected);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return timingSafeEqual(providedBuf, expected);
|
|
84
|
+
}
|
|
85
|
+
function decodeBasicAuth(header) {
|
|
86
|
+
if (!header.toLowerCase().startsWith("basic "))
|
|
87
|
+
return null;
|
|
88
|
+
const encoded = header.slice(6).trim();
|
|
89
|
+
if (!encoded)
|
|
90
|
+
return null;
|
|
91
|
+
let decoded;
|
|
92
|
+
try {
|
|
93
|
+
decoded = Buffer.from(encoded, "base64").toString("utf8");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const colon = decoded.indexOf(":");
|
|
99
|
+
if (colon === -1)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
username: decoded.slice(0, colon),
|
|
103
|
+
password: decoded.slice(colon + 1),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { WebAuth } from "./web-auth.js";
|
|
2
|
+
type NodeEventSource = {
|
|
3
|
+
on(event: string, listener: (...args: any[]) => void): unknown;
|
|
4
|
+
};
|
|
5
|
+
export declare function attachNodeDisconnectHandlers(req: NodeEventSource & {
|
|
6
|
+
aborted: boolean;
|
|
7
|
+
complete: boolean;
|
|
8
|
+
socket: NodeEventSource;
|
|
9
|
+
}, res: NodeEventSource & {
|
|
10
|
+
writableEnded: boolean;
|
|
11
|
+
}, onDisconnect: () => void): void;
|
|
12
|
+
/**
|
|
13
|
+
* Port fallback policy for the takeover loop. A port that fails to bind AND
|
|
14
|
+
* answers no HTTP is treated as orphaned Windows kernel residue; after
|
|
15
|
+
* `minFailedTakeovers` consecutive failed takeovers the web server moves to
|
|
16
|
+
* `currentPort + 1`, bounded by `maxFallbackPort`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function nextFallbackPort(currentPort: number, failedTakeovers: number, maxFallbackPort: number, minFailedTakeovers?: number): number;
|
|
19
|
+
interface WebServerConfig {
|
|
20
|
+
port: number;
|
|
21
|
+
host: string;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
auth?: WebAuth;
|
|
24
|
+
apiToken?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class WebServer {
|
|
27
|
+
private server;
|
|
28
|
+
private config;
|
|
29
|
+
private isOwner;
|
|
30
|
+
private startPromise;
|
|
31
|
+
private healthCheckInterval;
|
|
32
|
+
private onTakeoverCallback;
|
|
33
|
+
private onPortsExhaustedCallback;
|
|
34
|
+
private portsExhaustedNotified;
|
|
35
|
+
private takeoverFailures;
|
|
36
|
+
private readonly maxFallbackPort;
|
|
37
|
+
constructor(config: WebServerConfig);
|
|
38
|
+
setOnTakeoverCallback(callback: () => Promise<void>): void;
|
|
39
|
+
setOnPortsExhaustedCallback(callback: () => void): void;
|
|
40
|
+
start(): Promise<void>;
|
|
41
|
+
private _start;
|
|
42
|
+
private startHealthCheckLoop;
|
|
43
|
+
private stopHealthCheckLoop;
|
|
44
|
+
private attemptTakeover;
|
|
45
|
+
private notifyPortsExhausted;
|
|
46
|
+
stop(): Promise<void>;
|
|
47
|
+
isRunning(): boolean;
|
|
48
|
+
isServerOwner(): boolean;
|
|
49
|
+
getUrl(): string;
|
|
50
|
+
checkServerAvailable(): Promise<boolean>;
|
|
51
|
+
private handleRequest;
|
|
52
|
+
private contentTypeFor;
|
|
53
|
+
private serveStaticFile;
|
|
54
|
+
private jsonResponse;
|
|
55
|
+
}
|
|
56
|
+
export declare function startWebServer(config: WebServerConfig): Promise<WebServer>;
|
|
57
|
+
export {};
|