@alfe.ai/openclaw-knowledge 0.0.1
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/dist/index.cjs +2 -0
- package/dist/index.d.cts +117 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.d.cts +58 -0
- package/dist/plugin.d.cts.map +1 -0
- package/dist/plugin.d.ts +58 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +2 -0
- package/dist/plugin2.cjs +358 -0
- package/dist/plugin2.js +355 -0
- package/dist/plugin2.js.map +1 -0
- package/openclaw.plugin.json +45 -0
- package/package.json +40 -0
package/dist/plugin2.cjs
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
let _alfe_ai_config = require("@alfe.ai/config");
|
|
2
|
+
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
3
|
+
let node_module = require("node:module");
|
|
4
|
+
//#region src/formatter.ts
|
|
5
|
+
const SNIPPET_MAX = 240;
|
|
6
|
+
function snippet(text) {
|
|
7
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
8
|
+
return clean.length > SNIPPET_MAX ? `${clean.slice(0, SNIPPET_MAX)}…` : clean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Format RAG search hits for a tool return. Each hit carries provenance so
|
|
12
|
+
* the agent can follow up: docs point at a `filePath` (already mirrored to
|
|
13
|
+
* `shared/<scope>/`), facts at a `factId`.
|
|
14
|
+
*/
|
|
15
|
+
function formatSearchResults(result) {
|
|
16
|
+
if (result.results.length === 0) return "No matching knowledge found in your scopes.";
|
|
17
|
+
const lines = [];
|
|
18
|
+
for (const hit of result.results) {
|
|
19
|
+
const where = `${hit.scopeType}:${hit.scopeId}`;
|
|
20
|
+
const ref = hit.source === "doc" ? `doc ${hit.filePath ?? "?"}` : `fact ${hit.factId ?? "?"}`;
|
|
21
|
+
lines.push(`- [${where}] (${ref}, score ${hit.score.toFixed(2)})\n ${snippet(hit.text)}`);
|
|
22
|
+
}
|
|
23
|
+
if (result.truncatedScopes) lines.push("(Note: you belong to more scopes than the search fan-out cap; narrow with scopeId to reach the rest.)");
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Bounded session-start context: the org profile (the headline "what is this
|
|
28
|
+
* org" surface) plus a NAME-ONLY list of every scope the agent belongs to.
|
|
29
|
+
* Full per-scope profiles are intentionally NOT fetched here — the agent
|
|
30
|
+
* pulls those on demand via `resource_get_profile`. Mirrors the memory
|
|
31
|
+
* plugin's tiered/capped auto-recall.
|
|
32
|
+
*/
|
|
33
|
+
function formatContextBlock(orgProfile, scopes, maxScopes) {
|
|
34
|
+
const parts = [];
|
|
35
|
+
if (orgProfile && (orgProfile.about || orgProfile.description)) {
|
|
36
|
+
parts.push("Organization profile:");
|
|
37
|
+
if (orgProfile.about) parts.push(`- About: ${snippet(orgProfile.about)}`);
|
|
38
|
+
if (orgProfile.description) parts.push(`- ${snippet(orgProfile.description)}`);
|
|
39
|
+
if (orgProfile.links.length > 0) parts.push(`- Links: ${orgProfile.links.map((l) => `${l.label} (${l.url})`).join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
if (scopes.length > 0) {
|
|
42
|
+
if (parts.length > 0) parts.push("");
|
|
43
|
+
parts.push("Your knowledge scopes (use resource_search / resource_get_profile with the scopeId):");
|
|
44
|
+
for (const s of scopes.slice(0, maxScopes)) parts.push(`- ${s.scopeType}: ${s.name} [scopeId: ${s.scopeId}]`);
|
|
45
|
+
if (scopes.length > maxScopes) parts.push(`- …and ${String(scopes.length - maxScopes)} more (call resource_list_scopes).`);
|
|
46
|
+
}
|
|
47
|
+
if (parts.length === 0) return void 0;
|
|
48
|
+
return `<knowledge-resources>\n${parts.join("\n")}\n</knowledge-resources>`;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/context.ts
|
|
52
|
+
/**
|
|
53
|
+
* Loads bounded knowledge context at session start: the org profile plus a
|
|
54
|
+
* name-only list of the agent's scopes. Called from `before_agent_start`.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately cheap and bounded — one `listScopes` call plus (at most) one
|
|
57
|
+
* org-profile fetch. Full per-scope profiles are NOT loaded; the agent pulls
|
|
58
|
+
* those on demand. Membership is fixed at connect time, so this is sent once
|
|
59
|
+
* per session and is stale until reconnect (mirrors SHARED_SCOPES).
|
|
60
|
+
*/
|
|
61
|
+
var AutoContext = class {
|
|
62
|
+
constructor(client, config, logger) {
|
|
63
|
+
this.client = client;
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.logger = logger;
|
|
66
|
+
}
|
|
67
|
+
async loadForPrompt() {
|
|
68
|
+
if (!this.config.injectContext) return void 0;
|
|
69
|
+
try {
|
|
70
|
+
const { scopes } = await this.client.listScopes();
|
|
71
|
+
if (scopes.length === 0) {
|
|
72
|
+
this.logger.debug("No knowledge scopes for agent; skipping context inject");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const orgScope = scopes.find((s) => s.scopeType === "org");
|
|
76
|
+
let orgProfile;
|
|
77
|
+
if (orgScope) try {
|
|
78
|
+
orgProfile = await this.client.getScopeProfile("org", orgScope.scopeId);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
this.logger.debug("Org profile fetch failed; injecting scopes only", { err: String(err) });
|
|
81
|
+
}
|
|
82
|
+
const block = formatContextBlock(orgProfile, scopes, this.config.maxScopes);
|
|
83
|
+
if (block) this.logger.debug("Loaded knowledge context for prompt", {
|
|
84
|
+
scopeCount: scopes.length,
|
|
85
|
+
hasOrgProfile: orgProfile !== void 0
|
|
86
|
+
});
|
|
87
|
+
return block;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
this.logger.warn("Failed to load knowledge context", { err: String(err) });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/plugin.ts
|
|
96
|
+
/**
|
|
97
|
+
* knowledge — OpenClaw knowledge-resources extension.
|
|
98
|
+
*
|
|
99
|
+
* Scoped, searchable knowledge ABOUT a thing being worked on (org / team /
|
|
100
|
+
* project). Distinct from private per-agent memory: this is SHARED,
|
|
101
|
+
* attributable, deliberate-publish knowledge that humans and other agents
|
|
102
|
+
* read and contribute to.
|
|
103
|
+
*
|
|
104
|
+
* Backed by:
|
|
105
|
+
* - services/knowledge — RAG search (vector index over docs + facts)
|
|
106
|
+
* - services/org — system of record + per-agent membership gate
|
|
107
|
+
* (docs, facts, profile)
|
|
108
|
+
*
|
|
109
|
+
* Registers:
|
|
110
|
+
* - Tools: resource_search, resource_read_doc, resource_write_doc,
|
|
111
|
+
* resource_write_fact, resource_get_profile, resource_list_scopes
|
|
112
|
+
* - Lifecycle hook: before_agent_start (bounded org-profile + scope-list inject)
|
|
113
|
+
*/
|
|
114
|
+
const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
115
|
+
const DEFAULT_CONFIG = {
|
|
116
|
+
injectContext: true,
|
|
117
|
+
maxScopes: 25
|
|
118
|
+
};
|
|
119
|
+
function resolvePluginConfig(pluginConfig) {
|
|
120
|
+
return {
|
|
121
|
+
injectContext: typeof pluginConfig?.injectContext === "boolean" ? pluginConfig.injectContext : DEFAULT_CONFIG.injectContext,
|
|
122
|
+
maxScopes: typeof pluginConfig?.maxScopes === "number" ? pluginConfig.maxScopes : DEFAULT_CONFIG.maxScopes
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const SCOPE_TYPE_PROP = {
|
|
126
|
+
type: "string",
|
|
127
|
+
enum: [
|
|
128
|
+
"org",
|
|
129
|
+
"team",
|
|
130
|
+
"project"
|
|
131
|
+
],
|
|
132
|
+
description: "Scope kind. For 'org' the scopeId is the tenantId. Get exact scopeIds from resource_list_scopes."
|
|
133
|
+
};
|
|
134
|
+
function asScopeType(value) {
|
|
135
|
+
return value === "org" || value === "team" || value === "project" ? value : void 0;
|
|
136
|
+
}
|
|
137
|
+
const plugin = {
|
|
138
|
+
id: "@alfe.ai/openclaw-knowledge",
|
|
139
|
+
name: "Knowledge Resources",
|
|
140
|
+
description: "Scoped, searchable org/team/project knowledge — docs, facts, and profiles",
|
|
141
|
+
version: pkg.version,
|
|
142
|
+
register(api) {
|
|
143
|
+
const config = resolvePluginConfig(api.pluginConfig);
|
|
144
|
+
const logger = api.logger;
|
|
145
|
+
const alfeConfig = (0, _alfe_ai_config.resolveConfig)();
|
|
146
|
+
const client = new _alfe_ai_agent_api_client.AgentApiClient({
|
|
147
|
+
apiKey: alfeConfig.apiKey,
|
|
148
|
+
apiUrl: alfeConfig.apiUrl
|
|
149
|
+
});
|
|
150
|
+
const autoContext = new AutoContext(client, config, logger);
|
|
151
|
+
api.registerTool(() => ({
|
|
152
|
+
name: "resource_search",
|
|
153
|
+
label: "Resource Search",
|
|
154
|
+
description: "Semantically search shared knowledge (docs + facts) across the org, teams, and projects you belong to. Returns provenance: doc hits point at a filePath, fact hits at a factId. Docs are ALSO already synced to your workspace under shared/<scope>/, so for a small corpus just read those files directly — search earns its keep on LARGE doc corpora and on published facts. Pass scopeType+scopeId to narrow to one scope; omit to search all your scopes.",
|
|
155
|
+
parameters: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
query: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "What to search for"
|
|
161
|
+
},
|
|
162
|
+
limit: {
|
|
163
|
+
type: "number",
|
|
164
|
+
description: "Maximum results (default 10, max 50)"
|
|
165
|
+
},
|
|
166
|
+
scopeType: SCOPE_TYPE_PROP,
|
|
167
|
+
scopeId: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "Narrow to one scope (from resource_list_scopes). Omit to search all."
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
required: ["query"]
|
|
173
|
+
},
|
|
174
|
+
execute: async (_toolCallId, params) => {
|
|
175
|
+
try {
|
|
176
|
+
return formatSearchResults(await client.knowledgeSearch(params.query, {
|
|
177
|
+
limit: params.limit,
|
|
178
|
+
scopeType: asScopeType(params.scopeType),
|
|
179
|
+
scopeId: typeof params.scopeId === "string" ? params.scopeId : void 0
|
|
180
|
+
}));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
return `Error searching knowledge: ${err instanceof Error ? err.message : String(err)}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}), { names: ["resource_search"] });
|
|
186
|
+
api.registerTool(() => ({
|
|
187
|
+
name: "resource_read_doc",
|
|
188
|
+
label: "Resource Read Doc",
|
|
189
|
+
description: "Read the full text of a knowledge doc in a scope. Docs are also mirrored to shared/<scope>/<filePath> in your workspace — prefer reading that local file when it exists; use this when you only have a search hit's filePath.",
|
|
190
|
+
parameters: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
scopeType: SCOPE_TYPE_PROP,
|
|
194
|
+
scopeId: {
|
|
195
|
+
type: "string",
|
|
196
|
+
description: "The scope's id (from resource_list_scopes)"
|
|
197
|
+
},
|
|
198
|
+
filePath: {
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "The doc's path within the scope (e.g. notes/datacenter.md)"
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
required: [
|
|
204
|
+
"scopeType",
|
|
205
|
+
"scopeId",
|
|
206
|
+
"filePath"
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
execute: async (_toolCallId, params) => {
|
|
210
|
+
const scopeType = asScopeType(params.scopeType);
|
|
211
|
+
if (!scopeType) return "Error: scopeType must be one of org, team, project.";
|
|
212
|
+
try {
|
|
213
|
+
const { text } = await client.readScopeDoc(scopeType, params.scopeId, params.filePath);
|
|
214
|
+
return text;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return `Error reading doc: ${err instanceof Error ? err.message : String(err)}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}), { names: ["resource_read_doc"] });
|
|
220
|
+
api.registerTool(() => ({
|
|
221
|
+
name: "resource_write_doc",
|
|
222
|
+
label: "Resource Write Doc",
|
|
223
|
+
description: "Create or overwrite a knowledge DOC in a scope. Use a DOC for durable, reshaped, human-readable prose that grows and is curated over time — runbooks, designs, onboarding notes, an evolving description of a system. Use resource_write_fact instead for a single crisp atomic statement. Writing is a DELIBERATE publish into shared, attributable knowledge (not your private memory). Overwriting an existing path creates a new revision (history is kept).",
|
|
224
|
+
parameters: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
scopeType: SCOPE_TYPE_PROP,
|
|
228
|
+
scopeId: {
|
|
229
|
+
type: "string",
|
|
230
|
+
description: "The scope's id (from resource_list_scopes)"
|
|
231
|
+
},
|
|
232
|
+
filePath: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: "Path within the scope, e.g. designs/data-center.md"
|
|
235
|
+
},
|
|
236
|
+
content: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "Full markdown content of the doc"
|
|
239
|
+
},
|
|
240
|
+
message: {
|
|
241
|
+
type: "string",
|
|
242
|
+
description: "Optional revision note describing the change"
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
required: [
|
|
246
|
+
"scopeType",
|
|
247
|
+
"scopeId",
|
|
248
|
+
"filePath",
|
|
249
|
+
"content"
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
execute: async (_toolCallId, params) => {
|
|
253
|
+
const scopeType = asScopeType(params.scopeType);
|
|
254
|
+
if (!scopeType) return "Error: scopeType must be one of org, team, project.";
|
|
255
|
+
try {
|
|
256
|
+
const { filePath } = await client.writeScopeDoc(scopeType, params.scopeId, params.filePath, params.content, { message: typeof params.message === "string" ? params.message : void 0 });
|
|
257
|
+
return `Wrote doc ${filePath} to ${scopeType}:${String(params.scopeId)}.`;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
return `Error writing doc: ${err instanceof Error ? err.message : String(err)}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}), { names: ["resource_write_doc"] });
|
|
263
|
+
api.registerTool(() => ({
|
|
264
|
+
name: "resource_write_fact",
|
|
265
|
+
label: "Resource Write Fact",
|
|
266
|
+
description: "Publish a single crisp FACT into a scope's shared knowledge. Use a FACT for one atomic, durable statement worth remembering across agents (e.g. 'The prod data center is in Hetzner FSN1', 'The billing owner is Kevin'). Use resource_write_doc instead for longer curated prose. This is a DELIBERATE, attributable cross-agent publish — it is NOT your private memory and is NOT an automatic merge. Keep it short and self-contained.",
|
|
267
|
+
parameters: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
scopeType: SCOPE_TYPE_PROP,
|
|
271
|
+
scopeId: {
|
|
272
|
+
type: "string",
|
|
273
|
+
description: "The scope's id (from resource_list_scopes)"
|
|
274
|
+
},
|
|
275
|
+
text: {
|
|
276
|
+
type: "string",
|
|
277
|
+
description: "The fact — one atomic, self-contained statement"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
required: [
|
|
281
|
+
"scopeType",
|
|
282
|
+
"scopeId",
|
|
283
|
+
"text"
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
execute: async (_toolCallId, params) => {
|
|
287
|
+
const scopeType = asScopeType(params.scopeType);
|
|
288
|
+
if (!scopeType) return "Error: scopeType must be one of org, team, project.";
|
|
289
|
+
try {
|
|
290
|
+
return `Published fact ${(await client.createScopeFact(scopeType, params.scopeId, params.text)).factId} to ${scopeType}:${String(params.scopeId)}.`;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return `Error publishing fact: ${err instanceof Error ? err.message : String(err)}`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}), { names: ["resource_write_fact"] });
|
|
296
|
+
api.registerTool(() => ({
|
|
297
|
+
name: "resource_get_profile",
|
|
298
|
+
label: "Resource Get Profile",
|
|
299
|
+
description: "Fetch the structured profile (about / description / links) for one scope. Use this to learn what a specific team or project is, on demand — the session-start context only lists scope names, not their full profiles.",
|
|
300
|
+
parameters: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {
|
|
303
|
+
scopeType: SCOPE_TYPE_PROP,
|
|
304
|
+
scopeId: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "The scope's id (from resource_list_scopes)"
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
required: ["scopeType", "scopeId"]
|
|
310
|
+
},
|
|
311
|
+
execute: async (_toolCallId, params) => {
|
|
312
|
+
const scopeType = asScopeType(params.scopeType);
|
|
313
|
+
if (!scopeType) return "Error: scopeType must be one of org, team, project.";
|
|
314
|
+
try {
|
|
315
|
+
const p = await client.getScopeProfile(scopeType, params.scopeId);
|
|
316
|
+
const lines = [`Profile for ${scopeType}:${String(params.scopeId)}`];
|
|
317
|
+
if (p.about) lines.push(`About: ${p.about}`);
|
|
318
|
+
if (p.description) lines.push(p.description);
|
|
319
|
+
if (p.links.length > 0) lines.push(`Links: ${p.links.map((l) => `${l.label} (${l.url})`).join(", ")}`);
|
|
320
|
+
if (lines.length === 1) lines.push("(no profile set)");
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
} catch (err) {
|
|
323
|
+
return `Error fetching profile: ${err instanceof Error ? err.message : String(err)}`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}), { names: ["resource_get_profile"] });
|
|
327
|
+
api.registerTool(() => ({
|
|
328
|
+
name: "resource_list_scopes",
|
|
329
|
+
label: "Resource List Scopes",
|
|
330
|
+
description: "List every knowledge scope you belong to (org + teams + projects) with each scope's exact scopeId. Call this to discover the scopeId to pass to the other resource_* tools (for 'org', scopeId is the tenantId).",
|
|
331
|
+
parameters: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {}
|
|
334
|
+
},
|
|
335
|
+
execute: async () => {
|
|
336
|
+
try {
|
|
337
|
+
const { scopes } = await client.listScopes();
|
|
338
|
+
if (scopes.length === 0) return "You do not belong to any knowledge scopes.";
|
|
339
|
+
return scopes.map((s) => `- ${s.scopeType}: ${s.name} [scopeId: ${s.scopeId}]`).join("\n");
|
|
340
|
+
} catch (err) {
|
|
341
|
+
return `Error listing scopes: ${err instanceof Error ? err.message : String(err)}`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}), { names: ["resource_list_scopes"] });
|
|
345
|
+
api.on("before_agent_start", async () => {
|
|
346
|
+
const block = await autoContext.loadForPrompt();
|
|
347
|
+
if (block) return { prependContext: block };
|
|
348
|
+
}, { priority: 10 });
|
|
349
|
+
logger.info("knowledge extension registered", { injectContext: config.injectContext });
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
//#endregion
|
|
353
|
+
Object.defineProperty(exports, "plugin", {
|
|
354
|
+
enumerable: true,
|
|
355
|
+
get: function() {
|
|
356
|
+
return plugin;
|
|
357
|
+
}
|
|
358
|
+
});
|