@unblocklabs/unblock-memory 0.3.3 → 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,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,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.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
- "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.4",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",