@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.
@@ -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
+ }
@@ -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({ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$" })),
29
- startedTo: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$" })),
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 ? { annotation: { ...annotation, scope: annotation.scope ?? "chunk" } } : {}),
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
- ? ["Use memory_search for relevant past facts, then memory_get when more surrounding context is needed."]
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), { names: ["memory_sync_sessions"] });
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), { names: ["memory_list_clusters"] });
404
- api.registerTool((ctx) => createFetchClusterTool(runtime, ctx), { names: ["memory_fetch_cluster"] });
405
- api.registerTool((ctx) => createListMaintenanceTool(runtime, ctx), { names: ["memory_list_maintenance_tasks"] });
406
- api.registerTool((ctx) => createUpdateMaintenanceTool(runtime, ctx), { names: ["memory_update_maintenance_task"] });
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 candidates = await runtime.searchSkills(active(context.agentId), buildSkillWhispererQuery(event.prompt, event.messages, config.historyMessages), config.minScore, CANDIDATE_LIMIT);
63
- const selected = candidates[0];
64
- if (!selected || selected.score < config.minScore)
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 previous = state.skills.get(selected.path);
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(selected.path) ?? {};
75
+ const history = state.skills.get(canonicalPath) ?? {};
71
76
  history.suggested = state.turn;
72
- state.skills.set(selected.path, history);
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
+ }
@@ -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;
@@ -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)
@@ -1,12 +1,34 @@
1
1
  {
2
2
  "id": "unblock-memory",
3
3
  "name": "Unblock Memory",
4
- "version": "0.3.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
- "contracts": { "tools": ["memory_search", "memory_get", "memory_sync_sessions", "memory_sync_status", "memory_recluster", "memory_list_clusters", "memory_fetch_cluster", "memory_list_maintenance_tasks", "memory_update_maintenance_task"] },
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/unblock-memory",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",