@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.
@@ -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
+ }
@@ -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 { cfg, agentId: ctx.agentId };
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({ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$" })),
18
- 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
+ })),
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 ? { annotation: { ...annotation, scope: annotation.scope ?? "chunk" } } : {}),
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
- ? ["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
+ ]
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), { names: ["memory_sync_sessions"] });
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), { names: ["memory_list_clusters"] });
391
- api.registerTool((ctx) => createFetchClusterTool(runtime, ctx), { names: ["memory_fetch_cluster"] });
392
- api.registerTool((ctx) => createListMaintenanceTool(runtime, ctx), { names: ["memory_list_maintenance_tasks"] });
393
- 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
+ });
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 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,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
+ }
@@ -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.2",
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
- "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.2",
3
+ "version": "0.3.4",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",