@unblocklabs/unblock-memory 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -1
- package/dist/src/analysis.js +21 -11
- package/dist/src/config.d.ts +15 -1
- package/dist/src/config.js +97 -13
- package/dist/src/contracts.d.ts +3 -0
- package/dist/src/manager.d.ts +2 -1
- package/dist/src/manager.js +47 -40
- package/dist/src/people-cli.d.ts +4 -0
- package/dist/src/people-cli.js +47 -0
- package/dist/src/people-evidence.d.ts +14 -0
- package/dist/src/people-evidence.js +93 -0
- package/dist/src/people-hooks.d.ts +5 -0
- package/dist/src/people-hooks.js +101 -0
- package/dist/src/people-refinement.d.ts +72 -0
- package/dist/src/people-refinement.js +254 -0
- package/dist/src/people-store.d.ts +134 -0
- package/dist/src/people-store.js +679 -0
- package/dist/src/people-tools.d.ts +5 -0
- package/dist/src/people-tools.js +190 -0
- package/dist/src/plugin.js +59 -16
- package/dist/src/skill-whisperer.js +11 -6
- package/dist/src/slack-directory.d.ts +37 -0
- package/dist/src/slack-directory.js +102 -0
- package/dist/src/sources.d.ts +1 -0
- package/dist/src/sources.js +7 -0
- package/openclaw.plugin.json +74 -3
- package/package.json +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import { renderPeopleWhisper } from "./people-hooks.js";
|
|
5
|
+
import { openClawSlackDirectory, syncSlackDirectory, } from "./slack-directory.js";
|
|
6
|
+
const nonEmpty = Type.String({ pattern: "\\S", maxLength: 1000 });
|
|
7
|
+
const personSelector = Type.Union([
|
|
8
|
+
Type.Object({ view: Type.Literal("person"), personId: nonEmpty }, { additionalProperties: false }),
|
|
9
|
+
Type.Object({
|
|
10
|
+
view: Type.Literal("person"),
|
|
11
|
+
identity: Type.Object({
|
|
12
|
+
provider: Type.Literal("slack"),
|
|
13
|
+
accountScope: nonEmpty,
|
|
14
|
+
externalId: nonEmpty,
|
|
15
|
+
}, { additionalProperties: false }),
|
|
16
|
+
}, { additionalProperties: false }),
|
|
17
|
+
]);
|
|
18
|
+
const inspectParameters = Type.Union([
|
|
19
|
+
personSelector,
|
|
20
|
+
Type.Object({
|
|
21
|
+
view: Type.Literal("todos"),
|
|
22
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
|
|
23
|
+
}, { additionalProperties: false }),
|
|
24
|
+
]);
|
|
25
|
+
const updateParameters = Type.Union([
|
|
26
|
+
Type.Object({
|
|
27
|
+
action: Type.Literal("set_policy"),
|
|
28
|
+
personId: nonEmpty,
|
|
29
|
+
refinementEnabled: Type.Optional(Type.Boolean()),
|
|
30
|
+
injectionEnabled: Type.Optional(Type.Boolean()),
|
|
31
|
+
}, { additionalProperties: false }),
|
|
32
|
+
Type.Object({
|
|
33
|
+
action: Type.Literal("set_company"),
|
|
34
|
+
personId: nonEmpty,
|
|
35
|
+
companyName: Type.String({ pattern: "\\S", maxLength: 500 }),
|
|
36
|
+
primaryDomain: Type.Optional(Type.String({ pattern: "\\S", maxLength: 500 })),
|
|
37
|
+
}, { additionalProperties: false }),
|
|
38
|
+
Type.Object({
|
|
39
|
+
action: Type.Literal("resolve_todo"),
|
|
40
|
+
deduplicationKey: nonEmpty,
|
|
41
|
+
note: Type.Optional(Type.String({ minLength: 1, maxLength: 500 })),
|
|
42
|
+
}, { additionalProperties: false }),
|
|
43
|
+
Type.Object({
|
|
44
|
+
action: Type.Literal("soft_delete_person"),
|
|
45
|
+
personId: nonEmpty,
|
|
46
|
+
}, { additionalProperties: false }),
|
|
47
|
+
Type.Object({
|
|
48
|
+
action: Type.Literal("restore_person"),
|
|
49
|
+
personId: nonEmpty,
|
|
50
|
+
}, { additionalProperties: false }),
|
|
51
|
+
]);
|
|
52
|
+
const syncParameters = Type.Object({
|
|
53
|
+
accountId: Type.String({ pattern: "\\S", maxLength: 200 }),
|
|
54
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),
|
|
55
|
+
}, { additionalProperties: false });
|
|
56
|
+
function context(ctx) {
|
|
57
|
+
const cfg = ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
|
|
58
|
+
return cfg && ctx.agentId ? { agentId: ctx.agentId } : undefined;
|
|
59
|
+
}
|
|
60
|
+
function personView(stores, agentId, selector, maxChars) {
|
|
61
|
+
const store = stores.get(agentId);
|
|
62
|
+
const person = "personId" in selector
|
|
63
|
+
? store.getPerson(selector.personId)
|
|
64
|
+
: store.findPersonByIdentity(selector.identity.provider, selector.identity.accountScope, selector.identity.externalId);
|
|
65
|
+
if (!person)
|
|
66
|
+
return { status: "not_found" };
|
|
67
|
+
const dossier = store.getDossier(person.id);
|
|
68
|
+
const contribution = person.status === "active" && person.injectionEnabled && dossier
|
|
69
|
+
? renderPeopleWhisper(dossier.dossier.blurb, maxChars)
|
|
70
|
+
: undefined;
|
|
71
|
+
return {
|
|
72
|
+
status: "ok",
|
|
73
|
+
person,
|
|
74
|
+
company: person.companyId ? store.getCompany(person.companyId) : undefined,
|
|
75
|
+
identities: store.listIdentities(person.id),
|
|
76
|
+
dossier,
|
|
77
|
+
injectionEligible: contribution !== undefined,
|
|
78
|
+
contribution,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function createInspectTool(stores, config, ctx) {
|
|
82
|
+
const active = context(ctx);
|
|
83
|
+
if (!active || ctx.senderIsOwner !== true)
|
|
84
|
+
return null;
|
|
85
|
+
return {
|
|
86
|
+
name: "memory_people_inspect",
|
|
87
|
+
label: "Inspect People Memory",
|
|
88
|
+
description: "Inspect one exact person or bounded actionable people todos.",
|
|
89
|
+
parameters: inspectParameters,
|
|
90
|
+
async execute(_toolCallId, raw) {
|
|
91
|
+
const input = Value.Parse(inspectParameters, raw);
|
|
92
|
+
if (input.view === "person") {
|
|
93
|
+
return jsonResult(personView(stores, active.agentId, input, config.whisperer.maxChars));
|
|
94
|
+
}
|
|
95
|
+
return jsonResult({
|
|
96
|
+
status: "ok",
|
|
97
|
+
todos: stores.get(active.agentId).listTodos(input.limit ?? 20),
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function createUpdateTool(stores, ctx) {
|
|
103
|
+
const active = context(ctx);
|
|
104
|
+
if (!active || ctx.senderIsOwner !== true)
|
|
105
|
+
return null;
|
|
106
|
+
return {
|
|
107
|
+
name: "memory_people_update",
|
|
108
|
+
label: "Update People Memory",
|
|
109
|
+
description: "Apply one validated people policy, company, todo, deletion, or restoration action.",
|
|
110
|
+
parameters: updateParameters,
|
|
111
|
+
async execute(_toolCallId, raw) {
|
|
112
|
+
if (ctx.senderIsOwner !== true)
|
|
113
|
+
return jsonResult({ status: "forbidden", error: "owner authorization required" });
|
|
114
|
+
const input = Value.Parse(updateParameters, raw);
|
|
115
|
+
const store = stores.get(active.agentId);
|
|
116
|
+
if (input.action === "set_policy") {
|
|
117
|
+
if (input.refinementEnabled === undefined && input.injectionEnabled === undefined) {
|
|
118
|
+
return jsonResult({
|
|
119
|
+
status: "invalid",
|
|
120
|
+
error: "set_policy requires at least one policy value",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const person = store.setPolicies(input.personId, input);
|
|
124
|
+
return jsonResult(person ? { status: "ok", person } : { status: "not_found" });
|
|
125
|
+
}
|
|
126
|
+
if (input.action === "set_company") {
|
|
127
|
+
const company = store.setCompany(input.personId, {
|
|
128
|
+
name: input.companyName,
|
|
129
|
+
primaryDomain: input.primaryDomain,
|
|
130
|
+
});
|
|
131
|
+
const person = store.getPerson(input.personId);
|
|
132
|
+
return jsonResult(company && person ? { status: "ok", company, person } : { status: "not_found" });
|
|
133
|
+
}
|
|
134
|
+
if (input.action === "resolve_todo") {
|
|
135
|
+
const todo = store.resolveTodoByKey(input.deduplicationKey, input.note);
|
|
136
|
+
return jsonResult(todo ? { status: "ok", todo } : { status: "not_found" });
|
|
137
|
+
}
|
|
138
|
+
if (input.action === "restore_person") {
|
|
139
|
+
const person = store.restorePerson(input.personId);
|
|
140
|
+
return jsonResult(person ? { status: "ok", person } : { status: "not_found" });
|
|
141
|
+
}
|
|
142
|
+
const person = store.softDeletePerson(input.personId);
|
|
143
|
+
return jsonResult(person ? { status: "ok", person } : { status: "not_found" });
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function createSyncTool(stores, reader, ctx) {
|
|
148
|
+
const active = context(ctx);
|
|
149
|
+
if (!active || ctx.senderIsOwner !== true)
|
|
150
|
+
return null;
|
|
151
|
+
return {
|
|
152
|
+
name: "memory_people_sync",
|
|
153
|
+
label: "Sync Slack People",
|
|
154
|
+
description: "Manually enrich this agent's people store from one OpenClaw-authenticated Slack directory account.",
|
|
155
|
+
parameters: syncParameters,
|
|
156
|
+
async execute(_toolCallId, raw) {
|
|
157
|
+
const input = Value.Parse(syncParameters, raw);
|
|
158
|
+
if (ctx.senderIsOwner !== true)
|
|
159
|
+
return jsonResult({ status: "forbidden", error: "owner authorization required" });
|
|
160
|
+
try {
|
|
161
|
+
return jsonResult(await syncSlackDirectory({
|
|
162
|
+
store: stores.get(active.agentId),
|
|
163
|
+
reader,
|
|
164
|
+
accountId: input.accountId.trim(),
|
|
165
|
+
limit: input.limit ?? 200,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return jsonResult({
|
|
170
|
+
status: "unavailable",
|
|
171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function registerPeopleTools(api, stores, config, directoryReader = openClawSlackDirectory) {
|
|
178
|
+
api.registerTool((ctx) => createInspectTool(stores, config, ctx), {
|
|
179
|
+
names: ["memory_people_inspect"],
|
|
180
|
+
optional: true,
|
|
181
|
+
});
|
|
182
|
+
api.registerTool((ctx) => createUpdateTool(stores, ctx), {
|
|
183
|
+
names: ["memory_people_update"],
|
|
184
|
+
optional: true,
|
|
185
|
+
});
|
|
186
|
+
api.registerTool((ctx) => createSyncTool(stores, directoryReader, ctx), {
|
|
187
|
+
names: ["memory_people_sync"],
|
|
188
|
+
optional: true,
|
|
189
|
+
});
|
|
190
|
+
}
|
package/dist/src/plugin.js
CHANGED
|
@@ -2,26 +2,41 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
4
|
import { resolveConfig } from "./config.js";
|
|
5
|
+
import { registerPeopleCli } from "./people-cli.js";
|
|
6
|
+
import { registerPeopleHooks } from "./people-hooks.js";
|
|
7
|
+
import { PeopleStores } from "./people-store.js";
|
|
8
|
+
import { registerPeopleTools } from "./people-tools.js";
|
|
5
9
|
import { QmdMemoryRuntime } from "./runtime.js";
|
|
6
10
|
import { registerSkillWhisperer } from "./skill-whisperer.js";
|
|
7
11
|
function getContext(ctx) {
|
|
8
12
|
const cfg = ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
|
|
9
13
|
if (!cfg || !ctx.agentId)
|
|
10
14
|
return undefined;
|
|
11
|
-
return {
|
|
15
|
+
return {
|
|
16
|
+
cfg,
|
|
17
|
+
agentId: ctx.agentId,
|
|
18
|
+
requestContext: {
|
|
19
|
+
sessionKey: ctx.sessionKey,
|
|
20
|
+
sessionId: ctx.sessionId,
|
|
21
|
+
messageChannel: ctx.messageChannel,
|
|
22
|
+
agentAccountId: ctx.agentAccountId,
|
|
23
|
+
nativeChannelId: ctx.nativeChannelId,
|
|
24
|
+
deliveryContext: ctx.deliveryContext,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
12
27
|
}
|
|
13
28
|
const searchParameters = Type.Object({
|
|
14
29
|
query: Type.String({ pattern: "\\S" }),
|
|
15
30
|
corpora: Type.Optional(Type.Array(Type.String({ pattern: "\\S" }), { minItems: 1 })),
|
|
16
31
|
sessionFilter: Type.Optional(Type.Object({
|
|
17
|
-
startedFrom: Type.Optional(Type.String({
|
|
18
|
-
|
|
32
|
+
startedFrom: Type.Optional(Type.String({
|
|
33
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$",
|
|
34
|
+
})),
|
|
35
|
+
startedTo: Type.Optional(Type.String({
|
|
36
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$",
|
|
37
|
+
})),
|
|
19
38
|
provider: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
20
|
-
chatType: Type.Optional(Type.Union([
|
|
21
|
-
Type.Literal("channel"),
|
|
22
|
-
Type.Literal("group"),
|
|
23
|
-
Type.Literal("direct"),
|
|
24
|
-
])),
|
|
39
|
+
chatType: Type.Optional(Type.Union([Type.Literal("channel"), Type.Literal("group"), Type.Literal("direct")])),
|
|
25
40
|
accountId: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
26
41
|
conversationId: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
27
42
|
}, { additionalProperties: false })),
|
|
@@ -47,7 +62,7 @@ function createSearchTool(runtime, ctx) {
|
|
|
47
62
|
description: "Search configured memory corpora with semantic vector retrieval. The isolated skills corpus is never included.",
|
|
48
63
|
parameters: searchParameters,
|
|
49
64
|
async execute(_toolCallId, params, signal) {
|
|
50
|
-
const { query: untrimmedQuery, corpora, sessionFilter, maxResults, minScore } = Value.Parse(searchParameters, params);
|
|
65
|
+
const { query: untrimmedQuery, corpora, sessionFilter, maxResults, minScore, } = Value.Parse(searchParameters, params);
|
|
51
66
|
const query = untrimmedQuery.trim();
|
|
52
67
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
53
68
|
if (!manager)
|
|
@@ -58,6 +73,7 @@ function createSearchTool(runtime, ctx) {
|
|
|
58
73
|
maxResults,
|
|
59
74
|
minScore,
|
|
60
75
|
signal,
|
|
76
|
+
requestContext: active.requestContext,
|
|
61
77
|
});
|
|
62
78
|
return jsonResult({
|
|
63
79
|
results: results.map((result) => result.session
|
|
@@ -93,6 +109,7 @@ function createGetTool(runtime, ctx) {
|
|
|
93
109
|
relPath: path,
|
|
94
110
|
from,
|
|
95
111
|
lines,
|
|
112
|
+
requestContext: active.requestContext,
|
|
96
113
|
}));
|
|
97
114
|
},
|
|
98
115
|
};
|
|
@@ -289,7 +306,9 @@ function createUpdateMaintenanceTool(runtime, ctx) {
|
|
|
289
306
|
id: taskId,
|
|
290
307
|
status: action === "resolve" ? "resolved" : action === "defer" ? "deferred" : "irrelevant",
|
|
291
308
|
note,
|
|
292
|
-
...(annotation
|
|
309
|
+
...(annotation
|
|
310
|
+
? { annotation: { ...annotation, scope: annotation.scope ?? "chunk" } }
|
|
311
|
+
: {}),
|
|
293
312
|
});
|
|
294
313
|
return jsonResult(updated ? { status: "ok", task: updated } : { status: "not_found" });
|
|
295
314
|
},
|
|
@@ -367,6 +386,9 @@ export function resolveFlushPlan(params = {}) {
|
|
|
367
386
|
}
|
|
368
387
|
export function registerUnblockMemory(api) {
|
|
369
388
|
const config = resolveConfig(api.pluginConfig);
|
|
389
|
+
registerPeopleCli(api, config.people);
|
|
390
|
+
if (api.registrationMode === "cli-metadata")
|
|
391
|
+
return;
|
|
370
392
|
const runtime = new QmdMemoryRuntime(config.corpora, {
|
|
371
393
|
analysisExecutable: config.analysis.executable,
|
|
372
394
|
keepEmbeddingModelWarm: config.keepEmbeddingModelWarm,
|
|
@@ -375,20 +397,41 @@ export function registerUnblockMemory(api) {
|
|
|
375
397
|
deterministicRecallToolName: "memory_search",
|
|
376
398
|
supportsPrivateTranscriptRecall: false,
|
|
377
399
|
promptBuilder: ({ availableTools }) => availableTools.has("memory_search")
|
|
378
|
-
? [
|
|
400
|
+
? [
|
|
401
|
+
"Use memory_search for relevant past facts, then memory_get when more surrounding context is needed.",
|
|
402
|
+
]
|
|
379
403
|
: [],
|
|
380
404
|
flushPlanResolver: resolveFlushPlan,
|
|
381
405
|
runtime,
|
|
382
406
|
};
|
|
383
407
|
api.registerMemoryCapability(capability);
|
|
408
|
+
if (config.people.enabled) {
|
|
409
|
+
const peopleStores = new PeopleStores({
|
|
410
|
+
maxOpenTodos: config.people.todos.maxOpen,
|
|
411
|
+
maxBlurbChars: config.people.whisperer.maxChars,
|
|
412
|
+
});
|
|
413
|
+
registerPeopleHooks(api, peopleStores, config.people);
|
|
414
|
+
registerPeopleTools(api, peopleStores, config.people);
|
|
415
|
+
api.on("gateway_stop", () => peopleStores.closeAll());
|
|
416
|
+
}
|
|
384
417
|
registerSkillWhisperer(api, runtime, config.skillWhisperer);
|
|
385
418
|
api.registerTool((ctx) => createSearchTool(runtime, ctx), { names: ["memory_search"] });
|
|
386
419
|
api.registerTool((ctx) => createGetTool(runtime, ctx), { names: ["memory_get"] });
|
|
387
|
-
api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), {
|
|
420
|
+
api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), {
|
|
421
|
+
names: ["memory_sync_sessions"],
|
|
422
|
+
});
|
|
388
423
|
api.registerTool((ctx) => createSyncStatusTool(runtime, ctx), { names: ["memory_sync_status"] });
|
|
389
424
|
api.registerTool((ctx) => createReclusterTool(runtime, ctx), { names: ["memory_recluster"] });
|
|
390
|
-
api.registerTool((ctx) => createListClustersTool(runtime, ctx), {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
api.registerTool((ctx) =>
|
|
425
|
+
api.registerTool((ctx) => createListClustersTool(runtime, ctx), {
|
|
426
|
+
names: ["memory_list_clusters"],
|
|
427
|
+
});
|
|
428
|
+
api.registerTool((ctx) => createFetchClusterTool(runtime, ctx), {
|
|
429
|
+
names: ["memory_fetch_cluster"],
|
|
430
|
+
});
|
|
431
|
+
api.registerTool((ctx) => createListMaintenanceTool(runtime, ctx), {
|
|
432
|
+
names: ["memory_list_maintenance_tasks"],
|
|
433
|
+
});
|
|
434
|
+
api.registerTool((ctx) => createUpdateMaintenanceTool(runtime, ctx), {
|
|
435
|
+
names: ["memory_update_maintenance_task"],
|
|
436
|
+
});
|
|
394
437
|
}
|
|
@@ -59,17 +59,22 @@ export function registerSkillWhisperer(api, runtime, config) {
|
|
|
59
59
|
state.lastRunId = context.runId;
|
|
60
60
|
state.turn += 1;
|
|
61
61
|
try {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
62
|
+
const runtimeParams = active(context.agentId);
|
|
63
|
+
const candidates = await runtime.searchSkills(runtimeParams, buildSkillWhispererQuery(event.prompt, event.messages, config.historyMessages), config.minScore, CANDIDATE_LIMIT);
|
|
64
|
+
const resolved = candidates.flatMap((candidate) => {
|
|
65
|
+
const canonicalPath = runtime.resolveSkillPath(runtimeParams, candidate.path);
|
|
66
|
+
return canonicalPath ? [{ candidate, canonicalPath }] : [];
|
|
67
|
+
})[0];
|
|
68
|
+
if (!resolved || resolved.candidate.score < config.minScore)
|
|
65
69
|
return;
|
|
66
|
-
const
|
|
70
|
+
const { candidate: selected, canonicalPath } = resolved;
|
|
71
|
+
const previous = state.skills.get(canonicalPath);
|
|
67
72
|
const lastSeen = Math.max(previous?.suggested ?? -Infinity, previous?.opened ?? -Infinity);
|
|
68
73
|
if (state.turn - lastSeen <= config.cooldownTurns)
|
|
69
74
|
return;
|
|
70
|
-
const history = state.skills.get(
|
|
75
|
+
const history = state.skills.get(canonicalPath) ?? {};
|
|
71
76
|
history.suggested = state.turn;
|
|
72
|
-
state.skills.set(
|
|
77
|
+
state.skills.set(canonicalPath, history);
|
|
73
78
|
return {
|
|
74
79
|
prependContext: `A potentially relevant skill is available: ${JSON.stringify(selected.name)} ` +
|
|
75
80
|
`at ${JSON.stringify(selected.path)}. Check it before proceeding if applicable.`,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PeopleStore } from "./people-store.js";
|
|
2
|
+
type SlackDirectoryEntry = {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
handle?: string;
|
|
6
|
+
avatarUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
export type SlackDirectoryReader = {
|
|
9
|
+
listUsers(params: {
|
|
10
|
+
accountId: string;
|
|
11
|
+
limit: number;
|
|
12
|
+
}): Promise<readonly SlackDirectoryEntry[]>;
|
|
13
|
+
};
|
|
14
|
+
type DirectoryCommand = (executable: string, args: readonly string[], options: {
|
|
15
|
+
maxBuffer: number;
|
|
16
|
+
}) => Promise<{
|
|
17
|
+
stdout: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function createOpenClawSlackDirectory(run?: DirectoryCommand): SlackDirectoryReader;
|
|
20
|
+
export declare const openClawSlackDirectory: SlackDirectoryReader;
|
|
21
|
+
export declare function syncSlackDirectory(params: {
|
|
22
|
+
store: PeopleStore;
|
|
23
|
+
reader: SlackDirectoryReader;
|
|
24
|
+
accountId: string;
|
|
25
|
+
limit: number;
|
|
26
|
+
syncedAt?: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
created: number;
|
|
29
|
+
updated: number;
|
|
30
|
+
unchanged: number;
|
|
31
|
+
skipped: number;
|
|
32
|
+
failed: number;
|
|
33
|
+
status: "ok";
|
|
34
|
+
accountId: string;
|
|
35
|
+
received: number;
|
|
36
|
+
}>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
function text(value, maxLength) {
|
|
5
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, maxLength) : undefined;
|
|
6
|
+
}
|
|
7
|
+
export function createOpenClawSlackDirectory(run = async (executable, args, options) => {
|
|
8
|
+
const result = await execFileAsync(executable, args, options);
|
|
9
|
+
return { stdout: result.stdout };
|
|
10
|
+
}) {
|
|
11
|
+
return {
|
|
12
|
+
async listUsers({ accountId, limit }) {
|
|
13
|
+
const { stdout } = await run("openclaw", [
|
|
14
|
+
"directory",
|
|
15
|
+
"peers",
|
|
16
|
+
"list",
|
|
17
|
+
"--channel",
|
|
18
|
+
"slack",
|
|
19
|
+
"--account",
|
|
20
|
+
accountId,
|
|
21
|
+
"--limit",
|
|
22
|
+
String(limit),
|
|
23
|
+
"--json",
|
|
24
|
+
], { maxBuffer: 1024 * 1024 });
|
|
25
|
+
const parsed = JSON.parse(stdout);
|
|
26
|
+
if (!Array.isArray(parsed))
|
|
27
|
+
throw new Error("OpenClaw Slack directory returned an invalid response");
|
|
28
|
+
return parsed.flatMap((value) => {
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
+
return [];
|
|
31
|
+
const entry = value;
|
|
32
|
+
if (entry.kind !== "user")
|
|
33
|
+
return [];
|
|
34
|
+
const prefixedId = text(entry.id, 200);
|
|
35
|
+
if (!prefixedId?.startsWith("user:"))
|
|
36
|
+
return [];
|
|
37
|
+
const id = text(prefixedId.slice("user:".length), 200);
|
|
38
|
+
const handle = text(entry.handle, 200);
|
|
39
|
+
return id
|
|
40
|
+
? [
|
|
41
|
+
{
|
|
42
|
+
id,
|
|
43
|
+
name: text(entry.name, 500),
|
|
44
|
+
handle: text(handle?.startsWith("@") ? handle.slice(1) : handle, 200),
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
: [];
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export const openClawSlackDirectory = createOpenClawSlackDirectory();
|
|
53
|
+
export async function syncSlackDirectory(params) {
|
|
54
|
+
const entries = await params.reader.listUsers({
|
|
55
|
+
accountId: params.accountId,
|
|
56
|
+
limit: params.limit,
|
|
57
|
+
});
|
|
58
|
+
const counts = { created: 0, updated: 0, unchanged: 0, skipped: 0, failed: 0 };
|
|
59
|
+
const syncedAt = params.syncedAt ?? new Date().toISOString();
|
|
60
|
+
for (const entry of entries.slice(0, params.limit)) {
|
|
61
|
+
const externalId = text(entry.id, 200);
|
|
62
|
+
if (!externalId) {
|
|
63
|
+
counts.skipped += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const existing = params.store.findIdentity("slack", params.accountId, externalId);
|
|
68
|
+
if (existing && params.store.getPerson(existing.personId)?.status !== "active") {
|
|
69
|
+
counts.skipped += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const changed = existing !== undefined &&
|
|
73
|
+
((entry.name !== undefined && entry.name !== existing.displayName) ||
|
|
74
|
+
(entry.handle !== undefined && entry.handle !== existing.handle) ||
|
|
75
|
+
(entry.avatarUrl !== undefined && entry.avatarUrl !== existing.avatarUrl));
|
|
76
|
+
const result = params.store.upsertIdentity({
|
|
77
|
+
provider: "slack",
|
|
78
|
+
accountScope: params.accountId,
|
|
79
|
+
externalId,
|
|
80
|
+
displayName: entry.name,
|
|
81
|
+
handle: entry.handle,
|
|
82
|
+
avatarUrl: entry.avatarUrl,
|
|
83
|
+
syncedAt,
|
|
84
|
+
});
|
|
85
|
+
if (result.created)
|
|
86
|
+
counts.created += 1;
|
|
87
|
+
else if (changed)
|
|
88
|
+
counts.updated += 1;
|
|
89
|
+
else
|
|
90
|
+
counts.unchanged += 1;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
counts.failed += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
status: "ok",
|
|
98
|
+
accountId: params.accountId,
|
|
99
|
+
received: entries.length,
|
|
100
|
+
...counts,
|
|
101
|
+
};
|
|
102
|
+
}
|
package/dist/src/sources.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare function resolveSource(workspaceDir: string, configuredPath: stri
|
|
|
13
13
|
export declare function resolveSessionSource(sessionsDir: string, chatTypes: readonly ChatType[]): ResolvedSource;
|
|
14
14
|
export declare function resolveSources(workspaceDir: string, corpora: readonly (FileCorpusConfig | SkillCorpusConfig)[]): ResolvedSource[];
|
|
15
15
|
export declare function resolveConfiguredSkillPath(workspaceDir: string, inputPath: string, sources: readonly ResolvedSource[]): string | undefined;
|
|
16
|
+
export declare function sourceMatchesPath(source: ResolvedSource, inputPath: string): boolean;
|
|
16
17
|
export declare function parseSafeVirtualPath(virtualPath: string, sources: ReadonlyMap<string, ResolvedSource>): {
|
|
17
18
|
source: ResolvedSource;
|
|
18
19
|
relativePath: string;
|
package/dist/src/sources.js
CHANGED
|
@@ -121,6 +121,13 @@ export function resolveConfiguredSkillPath(workspaceDir, inputPath, sources) {
|
|
|
121
121
|
}
|
|
122
122
|
return undefined;
|
|
123
123
|
}
|
|
124
|
+
export function sourceMatchesPath(source, inputPath) {
|
|
125
|
+
const target = resolve(inputPath);
|
|
126
|
+
const relativePath = relative(source.root, target);
|
|
127
|
+
if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath))
|
|
128
|
+
return false;
|
|
129
|
+
return picomatch.isMatch(relativePath.split(sep).join("/"), source.pattern, { dot: true });
|
|
130
|
+
}
|
|
124
131
|
export function parseSafeVirtualPath(virtualPath, sources) {
|
|
125
132
|
const match = /^qmd:\/\/([^/]+)\/(.+)$/.exec(virtualPath.trim());
|
|
126
133
|
if (!match)
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|
|
8
8
|
"skills": ["./skills"],
|
|
9
|
-
"
|
|
9
|
+
"cliCommands": [
|
|
10
|
+
{
|
|
11
|
+
"name": "unblock-memory",
|
|
12
|
+
"description": "Unblock Memory administration",
|
|
13
|
+
"hasSubcommands": true
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"contracts": {
|
|
17
|
+
"tools": [
|
|
18
|
+
"memory_search",
|
|
19
|
+
"memory_get",
|
|
20
|
+
"memory_sync_sessions",
|
|
21
|
+
"memory_sync_status",
|
|
22
|
+
"memory_recluster",
|
|
23
|
+
"memory_list_clusters",
|
|
24
|
+
"memory_fetch_cluster",
|
|
25
|
+
"memory_list_maintenance_tasks",
|
|
26
|
+
"memory_update_maintenance_task",
|
|
27
|
+
"memory_people_inspect",
|
|
28
|
+
"memory_people_update",
|
|
29
|
+
"memory_people_sync"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
10
32
|
"toolMetadata": {
|
|
11
33
|
"memory_sync_sessions": { "sideEffecting": true },
|
|
12
34
|
"memory_sync_status": { "replaySafe": true },
|
|
@@ -14,7 +36,10 @@
|
|
|
14
36
|
"memory_list_clusters": { "replaySafe": true },
|
|
15
37
|
"memory_fetch_cluster": { "replaySafe": true },
|
|
16
38
|
"memory_list_maintenance_tasks": { "replaySafe": true },
|
|
17
|
-
"memory_update_maintenance_task": { "sideEffecting": true }
|
|
39
|
+
"memory_update_maintenance_task": { "sideEffecting": true },
|
|
40
|
+
"memory_people_inspect": { "replaySafe": true, "optional": true },
|
|
41
|
+
"memory_people_update": { "sideEffecting": true, "optional": true },
|
|
42
|
+
"memory_people_sync": { "sideEffecting": true, "optional": true }
|
|
18
43
|
},
|
|
19
44
|
"uiHints": {
|
|
20
45
|
"keepEmbeddingModelWarm": {
|
|
@@ -29,6 +54,14 @@
|
|
|
29
54
|
"label": "Skill Whisperer",
|
|
30
55
|
"help": "Suggest at most one semantically relevant configured skill before a user turn. Requires hook conversation access."
|
|
31
56
|
},
|
|
57
|
+
"people.enabled": {
|
|
58
|
+
"label": "PeopleSQL",
|
|
59
|
+
"help": "Maintain an agent-local people store. Disabled by default."
|
|
60
|
+
},
|
|
61
|
+
"people.whisperer.enabled": {
|
|
62
|
+
"label": "People Whisperer",
|
|
63
|
+
"help": "Inject an enabled person's bounded dossier blurb after exact identity matching. Requires hook conversation access."
|
|
64
|
+
},
|
|
32
65
|
"analysis.executable": {
|
|
33
66
|
"label": "Memory Analysis Worker",
|
|
34
67
|
"help": "Optional absolute path to the locally installed unblock-memory-analysis executable."
|
|
@@ -107,6 +140,44 @@
|
|
|
107
140
|
"executable": { "type": "string", "minLength": 1 }
|
|
108
141
|
}
|
|
109
142
|
},
|
|
143
|
+
"people": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"additionalProperties": false,
|
|
146
|
+
"properties": {
|
|
147
|
+
"enabled": { "type": "boolean", "default": false },
|
|
148
|
+
"refinement": {
|
|
149
|
+
"type": "object",
|
|
150
|
+
"additionalProperties": false,
|
|
151
|
+
"properties": {
|
|
152
|
+
"maxPeoplePerRun": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 }
|
|
153
|
+
},
|
|
154
|
+
"default": { "maxPeoplePerRun": 10 }
|
|
155
|
+
},
|
|
156
|
+
"whisperer": {
|
|
157
|
+
"type": "object",
|
|
158
|
+
"additionalProperties": false,
|
|
159
|
+
"properties": {
|
|
160
|
+
"enabled": { "type": "boolean", "default": false },
|
|
161
|
+
"maxChars": { "type": "integer", "minimum": 1, "maximum": 4000, "default": 1200 }
|
|
162
|
+
},
|
|
163
|
+
"default": { "enabled": false, "maxChars": 1200 }
|
|
164
|
+
},
|
|
165
|
+
"todos": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"additionalProperties": false,
|
|
168
|
+
"properties": {
|
|
169
|
+
"maxOpen": { "type": "integer", "minimum": 1, "maximum": 10000, "default": 1000 }
|
|
170
|
+
},
|
|
171
|
+
"default": { "maxOpen": 1000 }
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"default": {
|
|
175
|
+
"enabled": false,
|
|
176
|
+
"refinement": { "maxPeoplePerRun": 10 },
|
|
177
|
+
"whisperer": { "enabled": false, "maxChars": 1200 },
|
|
178
|
+
"todos": { "maxOpen": 1000 }
|
|
179
|
+
}
|
|
180
|
+
},
|
|
110
181
|
"skillWhisperer": {
|
|
111
182
|
"type": "object",
|
|
112
183
|
"additionalProperties": false,
|