@unblocklabs/unblock-memory 0.3.3 → 0.3.5
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/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 +193 -0
- package/dist/src/plugin.js +45 -15
- package/dist/src/skill-whisperer.js +11 -6
- package/dist/src/slack-directory.d.ts +47 -0
- package/dist/src/slack-directory.js +130 -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,193 @@
|
|
|
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 { createOpenClawSlackDirectory, 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) {
|
|
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 ??
|
|
187
|
+
createOpenClawSlackDirectory({
|
|
188
|
+
getConfig: () => ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config,
|
|
189
|
+
}), ctx), {
|
|
190
|
+
names: ["memory_people_sync"],
|
|
191
|
+
optional: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
package/dist/src/plugin.js
CHANGED
|
@@ -2,6 +2,10 @@ 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) {
|
|
@@ -25,14 +29,14 @@ const searchParameters = Type.Object({
|
|
|
25
29
|
query: Type.String({ pattern: "\\S" }),
|
|
26
30
|
corpora: Type.Optional(Type.Array(Type.String({ pattern: "\\S" }), { minItems: 1 })),
|
|
27
31
|
sessionFilter: Type.Optional(Type.Object({
|
|
28
|
-
startedFrom: Type.Optional(Type.String({
|
|
29
|
-
|
|
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
|
+
})),
|
|
30
38
|
provider: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
31
|
-
chatType: Type.Optional(Type.Union([
|
|
32
|
-
Type.Literal("channel"),
|
|
33
|
-
Type.Literal("group"),
|
|
34
|
-
Type.Literal("direct"),
|
|
35
|
-
])),
|
|
39
|
+
chatType: Type.Optional(Type.Union([Type.Literal("channel"), Type.Literal("group"), Type.Literal("direct")])),
|
|
36
40
|
accountId: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
37
41
|
conversationId: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
38
42
|
}, { additionalProperties: false })),
|
|
@@ -58,7 +62,7 @@ function createSearchTool(runtime, ctx) {
|
|
|
58
62
|
description: "Search configured memory corpora with semantic vector retrieval. The isolated skills corpus is never included.",
|
|
59
63
|
parameters: searchParameters,
|
|
60
64
|
async execute(_toolCallId, params, signal) {
|
|
61
|
-
const { query: untrimmedQuery, corpora, sessionFilter, maxResults, minScore } = Value.Parse(searchParameters, params);
|
|
65
|
+
const { query: untrimmedQuery, corpora, sessionFilter, maxResults, minScore, } = Value.Parse(searchParameters, params);
|
|
62
66
|
const query = untrimmedQuery.trim();
|
|
63
67
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
64
68
|
if (!manager)
|
|
@@ -302,7 +306,9 @@ function createUpdateMaintenanceTool(runtime, ctx) {
|
|
|
302
306
|
id: taskId,
|
|
303
307
|
status: action === "resolve" ? "resolved" : action === "defer" ? "deferred" : "irrelevant",
|
|
304
308
|
note,
|
|
305
|
-
...(annotation
|
|
309
|
+
...(annotation
|
|
310
|
+
? { annotation: { ...annotation, scope: annotation.scope ?? "chunk" } }
|
|
311
|
+
: {}),
|
|
306
312
|
});
|
|
307
313
|
return jsonResult(updated ? { status: "ok", task: updated } : { status: "not_found" });
|
|
308
314
|
},
|
|
@@ -380,6 +386,9 @@ export function resolveFlushPlan(params = {}) {
|
|
|
380
386
|
}
|
|
381
387
|
export function registerUnblockMemory(api) {
|
|
382
388
|
const config = resolveConfig(api.pluginConfig);
|
|
389
|
+
registerPeopleCli(api, config.people);
|
|
390
|
+
if (api.registrationMode === "cli-metadata")
|
|
391
|
+
return;
|
|
383
392
|
const runtime = new QmdMemoryRuntime(config.corpora, {
|
|
384
393
|
analysisExecutable: config.analysis.executable,
|
|
385
394
|
keepEmbeddingModelWarm: config.keepEmbeddingModelWarm,
|
|
@@ -388,20 +397,41 @@ export function registerUnblockMemory(api) {
|
|
|
388
397
|
deterministicRecallToolName: "memory_search",
|
|
389
398
|
supportsPrivateTranscriptRecall: false,
|
|
390
399
|
promptBuilder: ({ availableTools }) => availableTools.has("memory_search")
|
|
391
|
-
? [
|
|
400
|
+
? [
|
|
401
|
+
"Use memory_search for relevant past facts, then memory_get when more surrounding context is needed.",
|
|
402
|
+
]
|
|
392
403
|
: [],
|
|
393
404
|
flushPlanResolver: resolveFlushPlan,
|
|
394
405
|
runtime,
|
|
395
406
|
};
|
|
396
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
|
+
}
|
|
397
417
|
registerSkillWhisperer(api, runtime, config.skillWhisperer);
|
|
398
418
|
api.registerTool((ctx) => createSearchTool(runtime, ctx), { names: ["memory_search"] });
|
|
399
419
|
api.registerTool((ctx) => createGetTool(runtime, ctx), { names: ["memory_get"] });
|
|
400
|
-
api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), {
|
|
420
|
+
api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), {
|
|
421
|
+
names: ["memory_sync_sessions"],
|
|
422
|
+
});
|
|
401
423
|
api.registerTool((ctx) => createSyncStatusTool(runtime, ctx), { names: ["memory_sync_status"] });
|
|
402
424
|
api.registerTool((ctx) => createReclusterTool(runtime, ctx), { names: ["memory_recluster"] });
|
|
403
|
-
api.registerTool((ctx) => createListClustersTool(runtime, ctx), {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
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
|
+
});
|
|
407
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,47 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { PeopleStore } from "./people-store.js";
|
|
3
|
+
type SlackDirectoryEntry = {
|
|
4
|
+
id: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
handle?: string;
|
|
7
|
+
avatarUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
export type SlackDirectoryReader = {
|
|
10
|
+
listUsers(params: {
|
|
11
|
+
accountId: string;
|
|
12
|
+
limit: number;
|
|
13
|
+
}): Promise<readonly SlackDirectoryEntry[]>;
|
|
14
|
+
};
|
|
15
|
+
type SlackAccountInspector = (params: {
|
|
16
|
+
channelId: "slack";
|
|
17
|
+
cfg: OpenClawConfig;
|
|
18
|
+
accountId: string;
|
|
19
|
+
}) => Promise<Record<string, unknown> | null>;
|
|
20
|
+
type SlackRequest = (input: string | URL, init: {
|
|
21
|
+
headers: {
|
|
22
|
+
authorization: string;
|
|
23
|
+
};
|
|
24
|
+
signal: AbortSignal;
|
|
25
|
+
}) => Promise<Pick<Response, "json" | "ok" | "status">>;
|
|
26
|
+
export declare function createOpenClawSlackDirectory(params: {
|
|
27
|
+
getConfig: () => OpenClawConfig | undefined;
|
|
28
|
+
inspectAccount?: SlackAccountInspector;
|
|
29
|
+
request?: SlackRequest;
|
|
30
|
+
}): SlackDirectoryReader;
|
|
31
|
+
export declare function syncSlackDirectory(params: {
|
|
32
|
+
store: PeopleStore;
|
|
33
|
+
reader: SlackDirectoryReader;
|
|
34
|
+
accountId: string;
|
|
35
|
+
limit: number;
|
|
36
|
+
syncedAt?: string;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
created: number;
|
|
39
|
+
updated: number;
|
|
40
|
+
unchanged: number;
|
|
41
|
+
skipped: number;
|
|
42
|
+
failed: number;
|
|
43
|
+
status: "ok";
|
|
44
|
+
accountId: string;
|
|
45
|
+
received: number;
|
|
46
|
+
}>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { inspectReadOnlyChannelAccount } from "openclaw/plugin-sdk/directory-runtime";
|
|
2
|
+
function text(value, maxLength) {
|
|
3
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, maxLength) : undefined;
|
|
4
|
+
}
|
|
5
|
+
function record(value) {
|
|
6
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
function slackToken(account) {
|
|
11
|
+
return text(account.userToken, 10_000) ?? text(account.botToken, 10_000);
|
|
12
|
+
}
|
|
13
|
+
function slackEntry(value) {
|
|
14
|
+
const member = record(value);
|
|
15
|
+
const id = text(member?.id, 200);
|
|
16
|
+
if (!id)
|
|
17
|
+
return undefined;
|
|
18
|
+
const profile = record(member?.profile);
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
name: text(profile?.display_name, 500) ??
|
|
22
|
+
text(profile?.real_name, 500) ??
|
|
23
|
+
text(member?.real_name, 500) ??
|
|
24
|
+
text(member?.name, 500),
|
|
25
|
+
handle: text(member?.name, 200),
|
|
26
|
+
avatarUrl: text(profile?.image_512, 2_000) ??
|
|
27
|
+
text(profile?.image_192, 2_000) ??
|
|
28
|
+
text(profile?.image_72, 2_000),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function createOpenClawSlackDirectory(params) {
|
|
32
|
+
const inspectAccount = params.inspectAccount ?? inspectReadOnlyChannelAccount;
|
|
33
|
+
const request = params.request ?? fetch;
|
|
34
|
+
return {
|
|
35
|
+
async listUsers({ accountId, limit }) {
|
|
36
|
+
const cfg = params.getConfig();
|
|
37
|
+
if (!cfg)
|
|
38
|
+
throw new Error("OpenClaw runtime config is unavailable");
|
|
39
|
+
const account = await inspectAccount({ channelId: "slack", cfg, accountId });
|
|
40
|
+
const token = account ? slackToken(account) : undefined;
|
|
41
|
+
if (!token) {
|
|
42
|
+
throw new Error(`Slack credentials for account "${accountId}" are unavailable in the active runtime snapshot`);
|
|
43
|
+
}
|
|
44
|
+
const entries = [];
|
|
45
|
+
const cursors = new Set();
|
|
46
|
+
let cursor;
|
|
47
|
+
while (entries.length < limit) {
|
|
48
|
+
const url = new URL("https://slack.com/api/users.list");
|
|
49
|
+
url.searchParams.set("limit", String(Math.min(limit, 200)));
|
|
50
|
+
if (cursor)
|
|
51
|
+
url.searchParams.set("cursor", cursor);
|
|
52
|
+
const response = await request(url, {
|
|
53
|
+
headers: { authorization: `Bearer ${token}` },
|
|
54
|
+
signal: AbortSignal.timeout(30_000),
|
|
55
|
+
});
|
|
56
|
+
const payload = record(await response.json());
|
|
57
|
+
if (!response.ok || payload?.ok !== true) {
|
|
58
|
+
const detail = text(payload?.error, 200) ?? `HTTP ${response.status}`;
|
|
59
|
+
throw new Error(`Slack directory request failed: ${detail}`);
|
|
60
|
+
}
|
|
61
|
+
const members = Array.isArray(payload.members) ? payload.members : [];
|
|
62
|
+
for (const member of members) {
|
|
63
|
+
const entry = slackEntry(member);
|
|
64
|
+
if (entry)
|
|
65
|
+
entries.push(entry);
|
|
66
|
+
if (entries.length === limit)
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const next = text(record(payload.response_metadata)?.next_cursor, 2_000);
|
|
70
|
+
if (!next)
|
|
71
|
+
break;
|
|
72
|
+
if (cursors.has(next))
|
|
73
|
+
throw new Error("Slack directory returned a repeated cursor");
|
|
74
|
+
cursors.add(next);
|
|
75
|
+
cursor = next;
|
|
76
|
+
}
|
|
77
|
+
return entries;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function syncSlackDirectory(params) {
|
|
82
|
+
const entries = await params.reader.listUsers({
|
|
83
|
+
accountId: params.accountId,
|
|
84
|
+
limit: params.limit,
|
|
85
|
+
});
|
|
86
|
+
const counts = { created: 0, updated: 0, unchanged: 0, skipped: 0, failed: 0 };
|
|
87
|
+
const syncedAt = params.syncedAt ?? new Date().toISOString();
|
|
88
|
+
for (const entry of entries.slice(0, params.limit)) {
|
|
89
|
+
const externalId = text(entry.id, 200);
|
|
90
|
+
if (!externalId) {
|
|
91
|
+
counts.skipped += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const existing = params.store.findIdentity("slack", params.accountId, externalId);
|
|
96
|
+
if (existing && params.store.getPerson(existing.personId)?.status !== "active") {
|
|
97
|
+
counts.skipped += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const changed = existing !== undefined &&
|
|
101
|
+
((entry.name !== undefined && entry.name !== existing.displayName) ||
|
|
102
|
+
(entry.handle !== undefined && entry.handle !== existing.handle) ||
|
|
103
|
+
(entry.avatarUrl !== undefined && entry.avatarUrl !== existing.avatarUrl));
|
|
104
|
+
const result = params.store.upsertIdentity({
|
|
105
|
+
provider: "slack",
|
|
106
|
+
accountScope: params.accountId,
|
|
107
|
+
externalId,
|
|
108
|
+
displayName: entry.name,
|
|
109
|
+
handle: entry.handle,
|
|
110
|
+
avatarUrl: entry.avatarUrl,
|
|
111
|
+
syncedAt,
|
|
112
|
+
});
|
|
113
|
+
if (result.created)
|
|
114
|
+
counts.created += 1;
|
|
115
|
+
else if (changed)
|
|
116
|
+
counts.updated += 1;
|
|
117
|
+
else
|
|
118
|
+
counts.unchanged += 1;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
counts.failed += 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
status: "ok",
|
|
126
|
+
accountId: params.accountId,
|
|
127
|
+
received: entries.length,
|
|
128
|
+
...counts,
|
|
129
|
+
};
|
|
130
|
+
}
|
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.5",
|
|
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,
|