ei-tui 0.3.9 → 0.4.1
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/package.json +1 -1
- package/src/cli.ts +20 -30
- package/src/core/handlers/dedup.ts +29 -7
- package/src/core/handlers/rewrite.ts +7 -4
- package/src/core/orchestrators/dedup-phase.ts +46 -0
- package/src/core/orchestrators/index.ts +1 -1
- package/src/core/personas/opencode-agent.ts +8 -1
- package/src/core/processor.ts +10 -6
- package/src/core/state/human.ts +1 -1
- package/src/core/types/entities.ts +1 -1
- package/src/integrations/claude-code/importer.ts +3 -1
- package/src/integrations/claude-code/types.ts +1 -1
- package/src/integrations/cursor/importer.ts +3 -1
- package/src/integrations/cursor/types.ts +1 -1
- package/src/integrations/opencode/importer.ts +4 -2
- package/src/integrations/opencode/types.ts +39 -0
- package/src/integrations/process-check.ts +24 -0
- package/src/prompts/ceremony/index.ts +1 -0
- package/src/prompts/ceremony/user-dedup.ts +74 -0
- package/tui/README.md +1 -0
- package/tui/src/commands/dedupe.tsx +150 -0
- package/tui/src/components/PromptInput.tsx +4 -1
- package/tui/src/context/ei.tsx +9 -0
- package/tui/src/util/yaml-serializers.ts +3 -3
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -145,27 +145,17 @@ async function installClaudeCode(): Promise<void> {
|
|
|
145
145
|
const home = process.env.HOME || "~";
|
|
146
146
|
const claudeJsonPath = join(home, ".claude.json");
|
|
147
147
|
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
157
|
-
if (result.exitCode === 0) {
|
|
158
|
-
console.log(`✓ Registered Ei as Claude Code MCP server (user scope)`);
|
|
159
|
-
console.log(` Restart Claude Code to activate.`);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
console.warn(` claude mcp add failed (exit ${result.exitCode}), falling back to direct write`);
|
|
163
|
-
}
|
|
164
|
-
} catch {
|
|
165
|
-
// claude binary not found — fall through to direct write
|
|
166
|
-
}
|
|
148
|
+
// Claude Code supports ${VAR} substitution in env values, resolved from its
|
|
149
|
+
// own environment at spawn time — so the value stays fresh if EI_DATA_PATH changes.
|
|
150
|
+
const mcpEntry: Record<string, unknown> = {
|
|
151
|
+
type: "stdio",
|
|
152
|
+
command: "bunx",
|
|
153
|
+
args: ["ei-tui", "mcp"],
|
|
154
|
+
env: { EI_DATA_PATH: "${EI_DATA_PATH}" },
|
|
155
|
+
};
|
|
167
156
|
|
|
168
|
-
//
|
|
157
|
+
// Direct atomic write — we need full control over the config structure to
|
|
158
|
+
// write the env field. `claude mcp add` doesn't support env vars.
|
|
169
159
|
let config: Record<string, unknown> = {};
|
|
170
160
|
try {
|
|
171
161
|
const text = await Bun.file(claudeJsonPath).text();
|
|
@@ -175,11 +165,7 @@ async function installClaudeCode(): Promise<void> {
|
|
|
175
165
|
}
|
|
176
166
|
|
|
177
167
|
const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
|
|
178
|
-
mcpServers["ei"] =
|
|
179
|
-
type: "stdio",
|
|
180
|
-
command: "ei",
|
|
181
|
-
args: ["mcp"],
|
|
182
|
-
};
|
|
168
|
+
mcpServers["ei"] = mcpEntry;
|
|
183
169
|
config.mcpServers = mcpServers;
|
|
184
170
|
|
|
185
171
|
// Atomic write: write to temp file then rename to avoid partial writes
|
|
@@ -196,6 +182,14 @@ async function installCursor(): Promise<void> {
|
|
|
196
182
|
const home = process.env.HOME || "~";
|
|
197
183
|
const cursorJsonPath = join(home, ".cursor", "mcp.json");
|
|
198
184
|
|
|
185
|
+
// Cursor does not support ${VAR} substitution in mcp.json — literal values only.
|
|
186
|
+
const mcpEntry: Record<string, unknown> = {
|
|
187
|
+
type: "stdio",
|
|
188
|
+
command: "bunx",
|
|
189
|
+
args: ["ei-tui", "mcp"],
|
|
190
|
+
env: { EI_DATA_PATH: process.env.EI_DATA_PATH ?? "" },
|
|
191
|
+
};
|
|
192
|
+
|
|
199
193
|
let config: Record<string, unknown> = {};
|
|
200
194
|
try {
|
|
201
195
|
const text = await Bun.file(cursorJsonPath).text();
|
|
@@ -205,11 +199,7 @@ async function installCursor(): Promise<void> {
|
|
|
205
199
|
}
|
|
206
200
|
|
|
207
201
|
const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
|
|
208
|
-
mcpServers["ei"] =
|
|
209
|
-
type: "stdio",
|
|
210
|
-
command: "ei",
|
|
211
|
-
args: ["mcp"],
|
|
212
|
-
};
|
|
202
|
+
mcpServers["ei"] = mcpEntry;
|
|
213
203
|
config.mcpServers = mcpServers;
|
|
214
204
|
|
|
215
205
|
await Bun.$`mkdir -p ${join(home, ".cursor")}`;
|
|
@@ -48,7 +48,11 @@ export async function handleDedupCurate(
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
console.log(`[Dedup] Processing cluster: ${decisions.update.length} updates, ${decisions.remove.length} removals, ${decisions.add.length} additions`);
|
|
51
|
-
|
|
51
|
+
|
|
52
|
+
// Pre-compute: for each survivor (replaced_by), union the removed entity's groups.
|
|
53
|
+
// Must happen before any phase mutates state so we read the original values.
|
|
54
|
+
const groupsToMerge = new Map<string, { persona_groups: string[]; interested_personas: string[] }>();
|
|
55
|
+
|
|
52
56
|
// Map entity_type to pluralized state property name
|
|
53
57
|
const pluralMap: Record<string, 'facts' | 'topics' | 'people'> = {
|
|
54
58
|
fact: 'facts',
|
|
@@ -77,7 +81,20 @@ export async function handleDedupCurate(
|
|
|
77
81
|
console.warn(`[Dedup] No entities found for cluster (already merged?)`);
|
|
78
82
|
return;
|
|
79
83
|
}
|
|
80
|
-
|
|
84
|
+
|
|
85
|
+
for (const removal of decisions.remove) {
|
|
86
|
+
const removed = entities.find(e => e.id === removal.to_be_removed);
|
|
87
|
+
if (!removed) continue;
|
|
88
|
+
const acc = groupsToMerge.get(removal.replaced_by) ?? { persona_groups: [], interested_personas: [] };
|
|
89
|
+
groupsToMerge.set(removal.replaced_by, {
|
|
90
|
+
persona_groups: [...new Set([...acc.persona_groups, ...(removed.persona_groups ?? [])])],
|
|
91
|
+
interested_personas: [...new Set([...acc.interested_personas, ...(removed.interested_personas ?? [])])],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const clusterGroups = [...new Set(entities.flatMap(e => e.persona_groups ?? []))];
|
|
96
|
+
const clusterPersonas = [...new Set(entities.flatMap(e => e.interested_personas ?? []))];
|
|
97
|
+
|
|
81
98
|
// =========================================================================
|
|
82
99
|
// PHASE 1: Update Quote foreign keys FIRST (before deletions)
|
|
83
100
|
// =========================================================================
|
|
@@ -126,15 +143,20 @@ export async function handleDedupCurate(
|
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
const mergedFromRemoved = groupsToMerge.get(update.id);
|
|
147
|
+
const updatedEntity = {
|
|
131
148
|
...entity,
|
|
132
149
|
name: update.name ?? entity.name,
|
|
133
150
|
description: update.description ?? entity.description,
|
|
134
151
|
sentiment: update.sentiment ?? entity.sentiment,
|
|
135
152
|
last_updated: new Date().toISOString(),
|
|
136
153
|
embedding,
|
|
137
|
-
|
|
154
|
+
persona_groups: mergedFromRemoved
|
|
155
|
+
? [...new Set([...(entity.persona_groups ?? []), ...mergedFromRemoved.persona_groups])]
|
|
156
|
+
: entity.persona_groups,
|
|
157
|
+
interested_personas: mergedFromRemoved
|
|
158
|
+
? [...new Set([...(entity.interested_personas ?? []), ...mergedFromRemoved.interested_personas])]
|
|
159
|
+
: entity.interested_personas,
|
|
138
160
|
...(update.strength !== undefined && { strength: update.strength }),
|
|
139
161
|
...(update.confidence !== undefined && { confidence: update.confidence }),
|
|
140
162
|
...(update.exposure_current !== undefined && { exposure_current: update.exposure_current }),
|
|
@@ -194,7 +216,6 @@ export async function handleDedupCurate(
|
|
|
194
216
|
// Generate ID for new entity
|
|
195
217
|
const id = crypto.randomUUID();
|
|
196
218
|
|
|
197
|
-
// Build complete entity
|
|
198
219
|
const newEntity = {
|
|
199
220
|
id,
|
|
200
221
|
type: entity_type,
|
|
@@ -205,7 +226,8 @@ export async function handleDedupCurate(
|
|
|
205
226
|
learned_by: "ei",
|
|
206
227
|
last_changed_by: "ei",
|
|
207
228
|
embedding,
|
|
208
|
-
|
|
229
|
+
persona_groups: clusterGroups,
|
|
230
|
+
interested_personas: clusterPersonas,
|
|
209
231
|
...((entity_type === 'topic' || entity_type === 'person') && {
|
|
210
232
|
exposure_current: addition.exposure_current ?? 0.0,
|
|
211
233
|
exposure_desired: addition.exposure_desired ?? 0.5,
|
|
@@ -125,12 +125,14 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
125
125
|
const human = state.getHuman();
|
|
126
126
|
const now = new Date().toISOString();
|
|
127
127
|
|
|
128
|
-
// Look up the original item to inherit persona_groups
|
|
129
128
|
const allItems: DataItemBase[] = [
|
|
130
129
|
...human.topics, ...human.people,
|
|
131
130
|
];
|
|
132
|
-
|
|
133
|
-
const
|
|
131
|
+
|
|
132
|
+
const existingIds = new Set([itemId, ...(result.existing?.map(i => i.id) ?? [])]);
|
|
133
|
+
const involvedItems = allItems.filter(i => existingIds.has(i.id));
|
|
134
|
+
const unionGroups = [...new Set(involvedItems.flatMap(i => i.persona_groups ?? []))];
|
|
135
|
+
const unionPersonas = [...new Set(involvedItems.flatMap(i => i.interested_personas ?? []))];
|
|
134
136
|
|
|
135
137
|
// Helper: resolve actual type from existing records (don't trust LLM's type field)
|
|
136
138
|
const resolveExistingType = (id: string): RewriteItemType | null => {
|
|
@@ -217,7 +219,8 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
217
219
|
sentiment: item.sentiment ?? 0,
|
|
218
220
|
last_updated: now,
|
|
219
221
|
learned_by: "ei",
|
|
220
|
-
persona_groups:
|
|
222
|
+
persona_groups: unionGroups,
|
|
223
|
+
interested_personas: unionPersonas,
|
|
221
224
|
embedding,
|
|
222
225
|
};
|
|
223
226
|
|
|
@@ -2,6 +2,7 @@ import { StateManager } from "../state-manager.js";
|
|
|
2
2
|
import { LLMRequestType, LLMPriority, LLMNextStep, type DataItemBase } from "../types.js";
|
|
3
3
|
import type { DataItemType } from "../types/data-items.js";
|
|
4
4
|
import { buildDedupPrompt } from "../../prompts/ceremony/dedup.js";
|
|
5
|
+
import { buildUserDedupPrompt } from "../../prompts/ceremony/user-dedup.js";
|
|
5
6
|
|
|
6
7
|
// =============================================================================
|
|
7
8
|
// TYPES
|
|
@@ -201,3 +202,48 @@ export function queueDedupPhase(state: StateManager): void {
|
|
|
201
202
|
|
|
202
203
|
console.log(`[Dedup] Queued ${totalClusters} clusters for curation`);
|
|
203
204
|
}
|
|
205
|
+
|
|
206
|
+
// =============================================================================
|
|
207
|
+
// USER-TRIGGERED DEDUP
|
|
208
|
+
// =============================================================================
|
|
209
|
+
|
|
210
|
+
export function queueUserDedupRequest(
|
|
211
|
+
state: StateManager,
|
|
212
|
+
itemType: "topic" | "person",
|
|
213
|
+
entityIds: string[]
|
|
214
|
+
): void {
|
|
215
|
+
const human = state.getHuman();
|
|
216
|
+
|
|
217
|
+
const collection = (itemType === "topic" ? human.topics : human.people) as DedupableItem[];
|
|
218
|
+
const entities = entityIds
|
|
219
|
+
.map(id => collection.find(item => item.id === id))
|
|
220
|
+
.filter((item): item is DedupableItem => item !== undefined);
|
|
221
|
+
|
|
222
|
+
if (entities.length < 2) {
|
|
223
|
+
console.warn("[UserDedup] Need at least 2 entities to merge");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const prompt = buildUserDedupPrompt({
|
|
228
|
+
cluster: entities,
|
|
229
|
+
itemType,
|
|
230
|
+
similarityRange: { min: 1.0, max: 1.0 },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const model = human.settings?.rewrite_model ?? undefined;
|
|
234
|
+
|
|
235
|
+
state.queue_enqueue({
|
|
236
|
+
type: LLMRequestType.JSON,
|
|
237
|
+
priority: LLMPriority.High,
|
|
238
|
+
system: prompt.system,
|
|
239
|
+
user: prompt.user,
|
|
240
|
+
next_step: LLMNextStep.HandleDedupCurate,
|
|
241
|
+
...(model ? { model } : {}),
|
|
242
|
+
data: {
|
|
243
|
+
entity_type: itemType,
|
|
244
|
+
entity_ids: entityIds,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
console.log(`[UserDedup] Queued merge of ${entities.length} ${itemType} entities`);
|
|
249
|
+
}
|
|
@@ -22,7 +22,7 @@ export {
|
|
|
22
22
|
queueDescriptionCheck,
|
|
23
23
|
runHumanCeremony,
|
|
24
24
|
} from "./ceremony.js";
|
|
25
|
-
export { queueDedupPhase } from "./dedup-phase.js";
|
|
25
|
+
export { queueDedupPhase, queueUserDedupRequest } from "./dedup-phase.js";
|
|
26
26
|
export {
|
|
27
27
|
queuePersonaTopicScan,
|
|
28
28
|
queuePersonaTopicMatch,
|
|
@@ -19,7 +19,14 @@ function resolveCanonicalAgent(agentName: string): { canonical: string; aliases:
|
|
|
19
19
|
return { canonical, aliases: variants };
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
let name = agentName;
|
|
24
|
+
name = name.replace(/^ai-sdlc[:-]/, "");
|
|
25
|
+
name = name.replace(/\s*\([^)]+\)\s*$/, "").trim();
|
|
26
|
+
name = name.replace(/-/g, " ");
|
|
27
|
+
const canonical = name.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
28
|
+
|
|
29
|
+
return { canonical, aliases: [agentName] };
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export async function ensureAgentPersona(
|
package/src/core/processor.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { ContextStatus as ContextStatusEnum } from "./types.js";
|
|
|
31
31
|
import { registerReadMemoryExecutor, registerFileReadExecutor } from "./tools/index.js";
|
|
32
32
|
import { createReadMemoryExecutor } from "./tools/builtin/read-memory.js";
|
|
33
33
|
import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
|
|
34
|
-
import { shouldStartCeremony, startCeremony, handleCeremonyProgress } from "./orchestrators/index.js";
|
|
34
|
+
import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueUserDedupRequest } from "./orchestrators/index.js";
|
|
35
35
|
import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
|
|
36
36
|
|
|
37
37
|
// Static module imports
|
|
@@ -105,9 +105,9 @@ import {
|
|
|
105
105
|
} from "./queue-manager.js";
|
|
106
106
|
|
|
107
107
|
const DEFAULT_LOOP_INTERVAL_MS = 100;
|
|
108
|
-
const DEFAULT_OPENCODE_POLLING_MS =
|
|
109
|
-
const DEFAULT_CLAUDE_CODE_POLLING_MS =
|
|
110
|
-
const DEFAULT_CURSOR_POLLING_MS =
|
|
108
|
+
const DEFAULT_OPENCODE_POLLING_MS = 60000;
|
|
109
|
+
const DEFAULT_CLAUDE_CODE_POLLING_MS = 60000;
|
|
110
|
+
const DEFAULT_CURSOR_POLLING_MS = 60000;
|
|
111
111
|
|
|
112
112
|
let processorInstanceCount = 0;
|
|
113
113
|
|
|
@@ -649,7 +649,7 @@ export class Processor {
|
|
|
649
649
|
if (!human.settings.opencode) {
|
|
650
650
|
human.settings.opencode = {
|
|
651
651
|
integration: false,
|
|
652
|
-
polling_interval_ms:
|
|
652
|
+
polling_interval_ms: 60000,
|
|
653
653
|
};
|
|
654
654
|
modified = true;
|
|
655
655
|
}
|
|
@@ -657,7 +657,7 @@ export class Processor {
|
|
|
657
657
|
if (!human.settings.claudeCode) {
|
|
658
658
|
human.settings.claudeCode = {
|
|
659
659
|
integration: false,
|
|
660
|
-
polling_interval_ms:
|
|
660
|
+
polling_interval_ms: 60000,
|
|
661
661
|
};
|
|
662
662
|
modified = true;
|
|
663
663
|
}
|
|
@@ -1578,6 +1578,10 @@ const toolNextSteps = new Set([
|
|
|
1578
1578
|
return clearQueue(this.stateManager, this.queueProcessor);
|
|
1579
1579
|
}
|
|
1580
1580
|
|
|
1581
|
+
queueUserDedup(itemType: "topic" | "person", entityIds: string[]): void {
|
|
1582
|
+
queueUserDedupRequest(this.stateManager, itemType, entityIds);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1581
1585
|
async submitOneShot(guid: string, systemPrompt: string, userPrompt: string): Promise<void> {
|
|
1582
1586
|
return submitOneShot(
|
|
1583
1587
|
this.stateManager,
|
package/src/core/state/human.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface SyncCredentials {
|
|
|
13
13
|
|
|
14
14
|
export interface OpenCodeSettings {
|
|
15
15
|
integration?: boolean;
|
|
16
|
-
polling_interval_ms?: number; // Default:
|
|
16
|
+
polling_interval_ms?: number; // Default: 60000 (1 min)
|
|
17
17
|
extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
|
|
18
18
|
extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
|
|
19
19
|
last_sync?: string; // ISO timestamp
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
queueAllScans,
|
|
12
12
|
type ExtractionContext,
|
|
13
13
|
} from "../../core/orchestrators/human-extraction.js";
|
|
14
|
+
import { isProcessRunning } from "../process-check.js";
|
|
14
15
|
|
|
15
16
|
// =============================================================================
|
|
16
17
|
// Export Types
|
|
@@ -219,6 +220,7 @@ export async function importClaudeCodeSessions(
|
|
|
219
220
|
const settings = human.settings?.claudeCode;
|
|
220
221
|
const processedSessions = settings?.processed_sessions ?? {};
|
|
221
222
|
const now = Date.now();
|
|
223
|
+
const toolRunning = await isProcessRunning("claude");
|
|
222
224
|
|
|
223
225
|
let targetSession: ClaudeCodeSession | null = null;
|
|
224
226
|
|
|
@@ -228,7 +230,7 @@ export async function importClaudeCodeSessions(
|
|
|
228
230
|
const sessionLastMs = new Date(session.lastMessageAt).getTime();
|
|
229
231
|
const ageMs = now - sessionLastMs;
|
|
230
232
|
|
|
231
|
-
if (ageMs < MIN_SESSION_AGE_MS) continue;
|
|
233
|
+
if (ageMs < MIN_SESSION_AGE_MS && toolRunning) continue;
|
|
232
234
|
|
|
233
235
|
if (!lastImported) {
|
|
234
236
|
targetSession = session;
|
|
@@ -157,7 +157,7 @@ export const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
|
|
|
157
157
|
*/
|
|
158
158
|
export interface ClaudeCodeSettings {
|
|
159
159
|
integration?: boolean;
|
|
160
|
-
polling_interval_ms?: number; // Default:
|
|
160
|
+
polling_interval_ms?: number; // Default: 60000 (1 min)
|
|
161
161
|
extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
|
|
162
162
|
extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
|
|
163
163
|
last_sync?: string; // ISO timestamp
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MIN_SESSION_AGE_MS,
|
|
8
8
|
} from "./types.js";
|
|
9
9
|
import { CursorReader } from "./reader.js";
|
|
10
|
+
import { isProcessRunning } from "../process-check.js";
|
|
10
11
|
import {
|
|
11
12
|
queueAllScans,
|
|
12
13
|
type ExtractionContext,
|
|
@@ -184,6 +185,7 @@ export async function importCursorSessions(
|
|
|
184
185
|
const human = stateManager.getHuman();
|
|
185
186
|
const processedSessions = human.settings?.cursor?.processed_sessions ?? {};
|
|
186
187
|
const now = Date.now();
|
|
188
|
+
const toolRunning = await isProcessRunning("Cursor");
|
|
187
189
|
|
|
188
190
|
let targetSession: CursorSession | null = null;
|
|
189
191
|
|
|
@@ -191,7 +193,7 @@ export async function importCursorSessions(
|
|
|
191
193
|
const sessionLastMs = new Date(session.lastMessageAt).getTime();
|
|
192
194
|
const ageMs = now - sessionLastMs;
|
|
193
195
|
|
|
194
|
-
if (ageMs < MIN_SESSION_AGE_MS) continue;
|
|
196
|
+
if (ageMs < MIN_SESSION_AGE_MS && toolRunning) continue;
|
|
195
197
|
|
|
196
198
|
const lastImported = processedSessions[session.id];
|
|
197
199
|
if (lastImported && sessionLastMs <= new Date(lastImported).getTime()) continue;
|
|
@@ -133,7 +133,7 @@ export const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
|
|
|
133
133
|
*/
|
|
134
134
|
export interface CursorSettings {
|
|
135
135
|
integration?: boolean;
|
|
136
|
-
polling_interval_ms?: number; // Default:
|
|
136
|
+
polling_interval_ms?: number; // Default: 60000 (1 min)
|
|
137
137
|
last_sync?: string; // ISO timestamp
|
|
138
138
|
extraction_point?: string; // ISO timestamp — floor for session filtering
|
|
139
139
|
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
queueAllScans,
|
|
9
9
|
type ExtractionContext,
|
|
10
10
|
} from "../../core/orchestrators/human-extraction.js";
|
|
11
|
+
import { isProcessRunning } from "../process-check.js";
|
|
11
12
|
|
|
12
13
|
// =============================================================================
|
|
13
14
|
// Constants
|
|
@@ -125,19 +126,20 @@ export async function importOpenCodeSessions(
|
|
|
125
126
|
let targetSession: OpenCodeSession | null = null;
|
|
126
127
|
const MIN_SESSION_AGE_MS = 20 * 60 * 1000; // 20 minutes
|
|
127
128
|
const now = Date.now();
|
|
129
|
+
const toolRunning = await isProcessRunning("opencode");
|
|
128
130
|
|
|
129
131
|
for (const session of sortedSessions) {
|
|
130
132
|
const lastImported = processedSessions[session.id];
|
|
131
133
|
if (!lastImported) {
|
|
132
134
|
const ageMs = now - session.time.updated;
|
|
133
|
-
if (ageMs >= MIN_SESSION_AGE_MS) {
|
|
135
|
+
if (ageMs >= MIN_SESSION_AGE_MS || !toolRunning) {
|
|
134
136
|
targetSession = session;
|
|
135
137
|
break;
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
if (session.time.updated > new Date(lastImported).getTime()) {
|
|
139
141
|
const ageMs = now - session.time.updated;
|
|
140
|
-
if (ageMs >= MIN_SESSION_AGE_MS) {
|
|
142
|
+
if (ageMs >= MIN_SESSION_AGE_MS || !toolRunning) {
|
|
141
143
|
targetSession = session;
|
|
142
144
|
break;
|
|
143
145
|
}
|
|
@@ -175,6 +175,7 @@ export const AGENT_TO_AGENT_PREFIXES = [
|
|
|
175
175
|
* Value = array of variants that should resolve to this persona
|
|
176
176
|
*/
|
|
177
177
|
export const AGENT_ALIASES: Record<string, string[]> = {
|
|
178
|
+
// ── OhMyOpenCode primary agents ──────────────────────────────────────────
|
|
178
179
|
Sisyphus: [
|
|
179
180
|
"sisyphus",
|
|
180
181
|
"Sisyphus",
|
|
@@ -182,6 +183,44 @@ export const AGENT_ALIASES: Record<string, string[]> = {
|
|
|
182
183
|
"Planner-Sisyphus",
|
|
183
184
|
"planner-sisyphus",
|
|
184
185
|
],
|
|
186
|
+
Build: ["build", "Build"],
|
|
187
|
+
Plan: ["plan", "Plan"],
|
|
188
|
+
Atlas: [
|
|
189
|
+
"atlas",
|
|
190
|
+
"Atlas",
|
|
191
|
+
"atlas (plan executor)",
|
|
192
|
+
"Atlas (plan executor)",
|
|
193
|
+
],
|
|
194
|
+
Prometheus: [
|
|
195
|
+
"prometheus",
|
|
196
|
+
"Prometheus",
|
|
197
|
+
"prometheus (plan builder)",
|
|
198
|
+
"Prometheus (plan builder)",
|
|
199
|
+
],
|
|
200
|
+
Hephaestus: [
|
|
201
|
+
"hephaestus",
|
|
202
|
+
"Hephaestus",
|
|
203
|
+
"hephaestus (deep agent)",
|
|
204
|
+
"Hephaestus (deep agent)",
|
|
205
|
+
],
|
|
206
|
+
|
|
207
|
+
// ── ai-sdlc agents (RobotsAndPencils/ai-sdlc-claude-code-template) ───────
|
|
208
|
+
// Installed via scripts/install-opencode.sh as "ai-sdlc-{name}" in OpenCode.
|
|
209
|
+
// Bare names included for direct/custom installs.
|
|
210
|
+
Architect: ["ai-sdlc-architect", "architect"],
|
|
211
|
+
"Frontend Engineer": ["ai-sdlc-frontend-engineer", "frontend-engineer"],
|
|
212
|
+
"Backend Engineer": ["ai-sdlc-backend-engineer", "backend-engineer"],
|
|
213
|
+
"Code Reviewer": ["ai-sdlc-code-reviewer", "code-reviewer"],
|
|
214
|
+
"Database Engineer": ["ai-sdlc-database-engineer", "database-engineer"],
|
|
215
|
+
Debugger: ["ai-sdlc-debugger", "debugger"],
|
|
216
|
+
"DevOps Engineer": ["ai-sdlc-devops-engineer", "devops-engineer"],
|
|
217
|
+
"Mobile Engineer": ["ai-sdlc-mobile-engineer", "mobile-engineer"],
|
|
218
|
+
"QA Engineer": ["ai-sdlc-qa-engineer", "qa-engineer"],
|
|
219
|
+
"Security Reviewer": ["ai-sdlc-security-reviewer", "security-reviewer"],
|
|
220
|
+
"Spec Validator": ["ai-sdlc-spec-validator", "spec-validator"],
|
|
221
|
+
"Technical Writer": ["ai-sdlc-technical-writer", "technical-writer"],
|
|
222
|
+
"Test Writer": ["ai-sdlc-test-writer", "test-writer"],
|
|
223
|
+
"AI Engineer": ["ai-sdlc-ai-engineer", "ai-engineer"],
|
|
185
224
|
};
|
|
186
225
|
|
|
187
226
|
/**
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const isBrowser = typeof document !== "undefined";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if any process with the given name is currently running.
|
|
5
|
+
* Always returns true in browser environments (process inspection not available).
|
|
6
|
+
* On Windows uses `tasklist`; on macOS/Linux uses `pgrep -x`.
|
|
7
|
+
*/
|
|
8
|
+
export async function isProcessRunning(processName: string): Promise<boolean> {
|
|
9
|
+
if (isBrowser) return true;
|
|
10
|
+
try {
|
|
11
|
+
const CHILD_PROCESS = "child_process";
|
|
12
|
+
const { execSync } = await import(/* @vite-ignore */ CHILD_PROCESS);
|
|
13
|
+
if (process.platform === "win32") {
|
|
14
|
+
const out = execSync(
|
|
15
|
+
`tasklist /FI "IMAGENAME eq ${processName}.exe" /NH 2>NUL`
|
|
16
|
+
).toString();
|
|
17
|
+
return out.toLowerCase().includes(`${processName.toLowerCase()}.exe`);
|
|
18
|
+
}
|
|
19
|
+
execSync(`pgrep -x ${processName}`, { stdio: "ignore" });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -3,6 +3,7 @@ export { buildPersonaExplorePrompt } from "./explore.js";
|
|
|
3
3
|
export { buildDescriptionCheckPrompt } from "./description-check.js";
|
|
4
4
|
export { buildRewriteScanPrompt, buildRewritePrompt } from "./rewrite.js";
|
|
5
5
|
export { buildDedupPrompt } from "./dedup.js";
|
|
6
|
+
export { buildUserDedupPrompt } from "./user-dedup.js";
|
|
6
7
|
export type {
|
|
7
8
|
PersonaExpirePromptData,
|
|
8
9
|
PersonaExpireResult,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { DedupPromptData } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// USER-TRIGGERED DEDUP — Direct merge, no candidate-finding, no hedging
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prompt for user-confirmed deduplication.
|
|
9
|
+
*
|
|
10
|
+
* Unlike buildDedupPrompt (ceremony), this skips all decision-making — the user
|
|
11
|
+
* has already confirmed these entities are duplicates. Opus just merges.
|
|
12
|
+
* No hedging, no "our system BELIEVES these MAY be duplicates" language.
|
|
13
|
+
*/
|
|
14
|
+
export function buildUserDedupPrompt(data: DedupPromptData): { system: string; user: string } {
|
|
15
|
+
const typeLabel = data.itemType.charAt(0).toUpperCase() + data.itemType.slice(1);
|
|
16
|
+
|
|
17
|
+
const system = `You are merging duplicate ${typeLabel} records in a user's personal knowledge base. The user has manually confirmed that all records in this cluster refer to the same entity.
|
|
18
|
+
|
|
19
|
+
**YOUR PRIME DIRECTIVE: LOSE NO DATA.**
|
|
20
|
+
|
|
21
|
+
Your job is synthesis, not decision-making. Do not question whether these are duplicates — they are. Simply collapse them into one comprehensive, non-repetitive record.
|
|
22
|
+
|
|
23
|
+
### Merge Rules:
|
|
24
|
+
- Pick the most descriptive, commonly-used name as the canonical name
|
|
25
|
+
- Union all unique details from every description — if it was in any record, it belongs in the merged record
|
|
26
|
+
- Descriptions should be concise (under 300 chars) but complete — no detail left behind
|
|
27
|
+
- Numeric fields: strength/confidence → take HIGHER; sentiment → AVERAGE; exposure → take HIGHER
|
|
28
|
+
- relationship/category → pick most specific/accurate
|
|
29
|
+
|
|
30
|
+
### Output Format:
|
|
31
|
+
{
|
|
32
|
+
"update": [
|
|
33
|
+
/* The single merged canonical record with ALL fields preserved */
|
|
34
|
+
/* MUST include "id" (use the oldest/most-referenced record's ID), "type", "name", "description" */
|
|
35
|
+
],
|
|
36
|
+
"remove": [
|
|
37
|
+
{"to_be_removed": "uuid-of-duplicate", "replaced_by": "uuid-of-canonical-record"},
|
|
38
|
+
/* One entry per record being absorbed */
|
|
39
|
+
],
|
|
40
|
+
"add": []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Return raw JSON only. No markdown, no commentary.
|
|
44
|
+
|
|
45
|
+
${buildRecordFormatHint(data.itemType)}`;
|
|
46
|
+
|
|
47
|
+
const user = JSON.stringify({
|
|
48
|
+
cluster: data.cluster.map(stripEmbedding),
|
|
49
|
+
cluster_type: data.itemType,
|
|
50
|
+
user_confirmed: true,
|
|
51
|
+
}, null, 2);
|
|
52
|
+
|
|
53
|
+
return { system, user };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Helpers
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
function stripEmbedding<T extends { embedding?: unknown }>(item: T): Omit<T, "embedding"> {
|
|
61
|
+
const { embedding: _, ...rest } = item;
|
|
62
|
+
return rest as Omit<T, "embedding">;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildRecordFormatHint(itemType: string): string {
|
|
66
|
+
switch (itemType) {
|
|
67
|
+
case "person":
|
|
68
|
+
return `Person fields: id, type, name, description, sentiment (-1 to 1), relationship, exposure_current (0-1), exposure_desired (0-1), learned_by (optional), last_changed_by (optional)`;
|
|
69
|
+
case "topic":
|
|
70
|
+
return `Topic fields: id, type, name, description, sentiment (-1 to 1), category, exposure_current (0-1), exposure_desired (0-1), learned_by (optional), last_changed_by (optional)`;
|
|
71
|
+
default:
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
}
|
package/tui/README.md
CHANGED
|
@@ -87,6 +87,7 @@ All commands start with `/`. Append `!` to any command as a shorthand for `--for
|
|
|
87
87
|
|---------|---------|-------------|
|
|
88
88
|
| `/me` | | Edit all your data (facts, traits, topics, people) in `$EDITOR` |
|
|
89
89
|
| `/me <type>` | | Edit one type: `facts`, `traits`, `topics`, or `people` |
|
|
90
|
+
| `/dedupe <person\|topic> <term> [term2 ...]` | | Fuzzy-search and merge duplicate people or topics in `$EDITOR`. Unquoted words are individual OR terms; quoted strings match as exact phrases: `/dedupe person Flare "Jeremy Scherer"` finds records matching `Flare` OR `Jeremy Scherer` |
|
|
90
91
|
| `/settings` | `/set` | Edit your global settings in `$EDITOR` |
|
|
91
92
|
| `/setsync <user> <pass>` | `/ss` | Set sync credentials (triggers restart) |
|
|
92
93
|
| `/tools` | | Manage tool providers — enable/disable tools per persona |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Command } from "./registry.js";
|
|
2
|
+
import { spawnEditor } from "../util/editor.js";
|
|
3
|
+
import type { Topic, Person } from "../../../src/core/types.js";
|
|
4
|
+
|
|
5
|
+
const VALID_TYPES = ["person", "topic"] as const;
|
|
6
|
+
type DedupeType = typeof VALID_TYPES[number];
|
|
7
|
+
|
|
8
|
+
function buildDedupeYAML(type: DedupeType, terms: string[], entities: Array<Topic | Person>): string {
|
|
9
|
+
const termDisplay = terms.map(t => t.includes(" ") ? `"${t}"` : t).join(" | ");
|
|
10
|
+
const header = [
|
|
11
|
+
`# /dedupe ${type} ${terms.map(t => t.includes(" ") ? `"${t}"` : t).join(" ")}`,
|
|
12
|
+
`# Terms: ${termDisplay}`,
|
|
13
|
+
`# Found ${entities.length} match${entities.length === 1 ? "" : "es"}. DELETE blocks for entries to EXCLUDE from the merge.`,
|
|
14
|
+
`# Keep at least 2. Save to confirm, :q to cancel (Vim tip: :cq quits with error — same effect, but now you know it exists).`,
|
|
15
|
+
``,
|
|
16
|
+
].join("\n");
|
|
17
|
+
|
|
18
|
+
const blocks = entities.map(entity => {
|
|
19
|
+
const lines = [
|
|
20
|
+
`- id: "${entity.id}"`,
|
|
21
|
+
` name: "${entity.name.replace(/"/g, '\\"')}"`,
|
|
22
|
+
` description: "${entity.description.replace(/"/g, '\\"')}"`,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if ("relationship" in entity && entity.relationship) {
|
|
26
|
+
lines.push(` relationship: "${entity.relationship}"`);
|
|
27
|
+
}
|
|
28
|
+
if ("category" in entity && entity.category) {
|
|
29
|
+
lines.push(` category: "${entity.category}"`);
|
|
30
|
+
}
|
|
31
|
+
if (entity.learned_by) {
|
|
32
|
+
lines.push(` learned_by: "${entity.learned_by}"`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const date = entity.last_updated ? entity.last_updated.slice(0, 10) : "";
|
|
36
|
+
if (date) lines.push(` last_updated: "${date}"`);
|
|
37
|
+
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return header + blocks.join("\n\n") + "\n";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseDedupeYAML(content: string): string[] {
|
|
45
|
+
const ids: string[] = [];
|
|
46
|
+
const pattern = /^\s*- id:\s*"([^"]+)"/gm;
|
|
47
|
+
let match: RegExpExecArray | null;
|
|
48
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
49
|
+
ids.push(match[1]);
|
|
50
|
+
}
|
|
51
|
+
return ids;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fuzzySearch(entities: Array<Topic | Person>, query: string): Array<Topic | Person> {
|
|
55
|
+
const lower = query.toLowerCase();
|
|
56
|
+
return entities.filter(entity =>
|
|
57
|
+
entity.name.toLowerCase().includes(lower)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const dedupeCommand: Command = {
|
|
62
|
+
name: "dedupe",
|
|
63
|
+
aliases: [],
|
|
64
|
+
description: "Merge duplicate people or topics",
|
|
65
|
+
usage: '/dedupe <person|topic> <term> ["term 2" ...]',
|
|
66
|
+
|
|
67
|
+
async execute(args, ctx) {
|
|
68
|
+
const type = args[0]?.toLowerCase() as DedupeType | undefined;
|
|
69
|
+
|
|
70
|
+
if (!type || !VALID_TYPES.includes(type)) {
|
|
71
|
+
ctx.showNotification(
|
|
72
|
+
`Usage: /dedupe <person|topic> <term> ["term 2" ...]. Got: ${args[0] ?? "(none)"}`,
|
|
73
|
+
"error"
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const terms = args.slice(1);
|
|
79
|
+
if (terms.length === 0) {
|
|
80
|
+
ctx.showNotification(`Usage: /dedupe ${type} <term> ["term 2" ...] — at least one term required`, "error");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const human = await ctx.ei.getHuman();
|
|
85
|
+
|
|
86
|
+
if (!human.settings?.rewrite_model) {
|
|
87
|
+
ctx.showNotification(`/dedupe requires a Default Rewrite Model — set one in /settings`, "error");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pool: Array<Topic | Person> = type === "topic" ? human.topics : human.people;
|
|
92
|
+
|
|
93
|
+
const seen = new Set<string>();
|
|
94
|
+
const matches: Array<Topic | Person> = [];
|
|
95
|
+
for (const term of terms) {
|
|
96
|
+
for (const entity of fuzzySearch(pool, term)) {
|
|
97
|
+
if (!seen.has(entity.id)) {
|
|
98
|
+
seen.add(entity.id);
|
|
99
|
+
matches.push(entity);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (matches.length === 0) {
|
|
105
|
+
ctx.showNotification(`No ${type}s matching ${terms.map(t => `"${t}"`).join(" | ")}`, "info");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (matches.length === 1) {
|
|
110
|
+
ctx.showNotification(`Only 1 ${type} matched — need at least 2 to merge`, "info");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const yamlContent = buildDedupeYAML(type, terms, matches);
|
|
115
|
+
|
|
116
|
+
const result = await spawnEditor({
|
|
117
|
+
initialContent: yamlContent,
|
|
118
|
+
filename: "ei-dedupe.yaml",
|
|
119
|
+
renderer: ctx.renderer,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (result.aborted) {
|
|
123
|
+
ctx.showNotification("Dedupe cancelled", "info");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!result.success) {
|
|
128
|
+
ctx.showNotification("Editor failed to open", "error");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (result.content === null) {
|
|
133
|
+
ctx.showNotification("No changes — dedupe cancelled", "info");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const keptIds = parseDedupeYAML(result.content);
|
|
138
|
+
|
|
139
|
+
if (keptIds.length < 2) {
|
|
140
|
+
ctx.showNotification("Need at least 2 entries to merge — dedupe cancelled", "error");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
ctx.ei.queueUserDedup(type, keptIds);
|
|
145
|
+
ctx.showNotification(
|
|
146
|
+
`Queued merge of ${keptIds.length} ${type}s — Opus will synthesize at high priority`,
|
|
147
|
+
"info"
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
@@ -25,6 +25,7 @@ import { queueCommand } from "../commands/queue";
|
|
|
25
25
|
import { dlqCommand } from "../commands/dlq";
|
|
26
26
|
import { toolsCommand } from "../commands/tools";
|
|
27
27
|
import { authCommand } from '../commands/auth';
|
|
28
|
+
import { dedupeCommand } from "../commands/dedupe";
|
|
28
29
|
import { useOverlay } from "../context/overlay";
|
|
29
30
|
import { CommandSuggest } from "./CommandSuggest";
|
|
30
31
|
import { useKeyboard } from "@opentui/solid";
|
|
@@ -64,6 +65,7 @@ export function PromptInput() {
|
|
|
64
65
|
registerCommand(dlqCommand);
|
|
65
66
|
registerCommand(toolsCommand);
|
|
66
67
|
registerCommand(authCommand);
|
|
68
|
+
registerCommand(dedupeCommand);
|
|
67
69
|
|
|
68
70
|
let textareaRef: TextareaRenderable | undefined;
|
|
69
71
|
|
|
@@ -163,7 +165,8 @@ export function PromptInput() {
|
|
|
163
165
|
text.startsWith("/context") ||
|
|
164
166
|
text.startsWith("/messages") ||
|
|
165
167
|
text === "/queue" ||
|
|
166
|
-
text === "/dlq"
|
|
168
|
+
text === "/dlq" ||
|
|
169
|
+
text.startsWith("/dedupe");
|
|
167
170
|
|
|
168
171
|
if (!isEditorCmd && !opensEditorForData) {
|
|
169
172
|
textareaRef?.clear();
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -106,6 +106,7 @@ export interface EiContextValue {
|
|
|
106
106
|
getToolList: () => ToolDefinition[];
|
|
107
107
|
updateToolProvider: (id: string, updates: Partial<Omit<ToolProvider, 'id' | 'created_at'>>) => Promise<boolean>;
|
|
108
108
|
updateTool: (id: string, updates: Partial<Omit<ToolDefinition, 'id' | 'created_at'>>) => Promise<boolean>;
|
|
109
|
+
queueUserDedup: (itemType: "topic" | "person", entityIds: string[]) => void;
|
|
109
110
|
cleanupTimers: () => void;
|
|
110
111
|
}
|
|
111
112
|
const EiContext = createContext<EiContextValue>();
|
|
@@ -141,6 +142,11 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
141
142
|
}, 5000);
|
|
142
143
|
};
|
|
143
144
|
|
|
145
|
+
const queueUserDedup = (itemType: "topic" | "person", entityIds: string[]): void => {
|
|
146
|
+
if (!processor) return;
|
|
147
|
+
processor.queueUserDedup(itemType, entityIds);
|
|
148
|
+
};
|
|
149
|
+
|
|
144
150
|
const cleanupTimers = () => {
|
|
145
151
|
if (notificationTimer) {
|
|
146
152
|
clearTimeout(notificationTimer);
|
|
@@ -406,11 +412,13 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
406
412
|
const deleteMessages = async (personaId: string, messageIds: string[]): Promise<void> => {
|
|
407
413
|
if (!processor) return;
|
|
408
414
|
await processor.deleteMessages(personaId, messageIds);
|
|
415
|
+
setStore("messages", store.messages.filter(m => !messageIds.includes(m.id)));
|
|
409
416
|
};
|
|
410
417
|
|
|
411
418
|
const setMessageContextStatus = async (personaId: string, messageId: string, status: ContextStatus): Promise<void> => {
|
|
412
419
|
if (!processor) return;
|
|
413
420
|
await processor.setMessageContextStatus(personaId, messageId, status);
|
|
421
|
+
setStore("messages", store.messages.map(m => m.id === messageId ? { ...m, context_status: status } : m));
|
|
414
422
|
};
|
|
415
423
|
|
|
416
424
|
const recallPendingMessages = async (): Promise<string> => {
|
|
@@ -661,6 +669,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
661
669
|
getToolList,
|
|
662
670
|
updateToolProvider,
|
|
663
671
|
updateTool,
|
|
672
|
+
queueUserDedup,
|
|
664
673
|
cleanupTimers,
|
|
665
674
|
};
|
|
666
675
|
return (
|
|
@@ -565,7 +565,7 @@ export function settingsToYAML(settings: HumanSettings | undefined): string {
|
|
|
565
565
|
},
|
|
566
566
|
opencode: {
|
|
567
567
|
integration: settings?.opencode?.integration ?? false,
|
|
568
|
-
polling_interval_ms: settings?.opencode?.polling_interval_ms ??
|
|
568
|
+
polling_interval_ms: settings?.opencode?.polling_interval_ms ?? 60000,
|
|
569
569
|
extraction_model: settings?.opencode?.extraction_model ?? 'default',
|
|
570
570
|
extraction_token_limit: settings?.opencode?.extraction_token_limit ?? 'default',
|
|
571
571
|
last_sync: settings?.opencode?.last_sync ?? null,
|
|
@@ -573,7 +573,7 @@ export function settingsToYAML(settings: HumanSettings | undefined): string {
|
|
|
573
573
|
},
|
|
574
574
|
claudeCode: {
|
|
575
575
|
integration: settings?.claudeCode?.integration ?? false,
|
|
576
|
-
polling_interval_ms: settings?.claudeCode?.polling_interval_ms ??
|
|
576
|
+
polling_interval_ms: settings?.claudeCode?.polling_interval_ms ?? 60000,
|
|
577
577
|
extraction_model: settings?.claudeCode?.extraction_model ?? 'default',
|
|
578
578
|
extraction_token_limit: settings?.claudeCode?.extraction_token_limit ?? 'default',
|
|
579
579
|
last_sync: settings?.claudeCode?.last_sync ?? null,
|
|
@@ -581,7 +581,7 @@ export function settingsToYAML(settings: HumanSettings | undefined): string {
|
|
|
581
581
|
},
|
|
582
582
|
cursor: {
|
|
583
583
|
integration: settings?.cursor?.integration ?? false,
|
|
584
|
-
polling_interval_ms: settings?.cursor?.polling_interval_ms ??
|
|
584
|
+
polling_interval_ms: settings?.cursor?.polling_interval_ms ?? 60000,
|
|
585
585
|
last_sync: settings?.cursor?.last_sync ?? null,
|
|
586
586
|
extraction_point: settings?.cursor?.extraction_point ?? null,
|
|
587
587
|
},
|