openclaw-contextualai 1.0.0

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,32 @@
1
+ name: CI - Type Check, Format & Lint
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ jobs:
7
+ quality-checks:
8
+ name: Quality Checks
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Checkout code
12
+ uses: actions/checkout@v4
13
+
14
+ - name: Setup Node
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: "20"
18
+ cache: "npm"
19
+
20
+ - name: Install dependencies
21
+ run: npm ci
22
+
23
+ - name: Setup Biome
24
+ uses: biomejs/setup-biome@v2
25
+ with:
26
+ version: 2.3.8
27
+
28
+ - name: Run TypeScript type checking
29
+ run: npm run check-types
30
+
31
+ - name: Run Biome CI (format & lint)
32
+ run: npx biome ci .
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Contextual AI Plugin for OpenClaw
2
+
3
+ Connect OpenClaw to [Contextual AI](https://contextual.ai) agents: list your agents and let the LLM route user questions to the right agent.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install openclaw-contextualai
9
+ ```
10
+
11
+ Restart OpenClaw after installing.
12
+
13
+ ## Configuration
14
+
15
+ The only required value is your Contextual AI API key. Get one at [app.contextual.ai](https://app.contextual.ai) (API Keys in the dashboard).
16
+
17
+ **Option A – Environment variable:**
18
+
19
+ ```bash
20
+ export OPENCLAW_CONTEXTUALAI_API_KEY="your-contextual-ai-api-key"
21
+ ```
22
+
23
+ **Option B – In `openclaw.json`:**
24
+
25
+ ```json
26
+ {
27
+ "plugins": {
28
+ "entries": {
29
+ "openclaw-contextualai": {
30
+ "enabled": true,
31
+ "config": {
32
+ "apiKey": "your-contextual-ai-api-key"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## How to use
41
+
42
+ In any channel where OpenClaw is running:
43
+
44
+ - **List your agents** – e.g. *“What Contextual AI agents do I have?”* or *“List my Contextual AI agents.”*
45
+ - **Query an agent** – e.g. *“Ask the Materials Science Agent: What materials are best for high-temperature aerospace?”*
46
+
47
+ The model will list your agents, then send your question to the right one and reply with the agent’s answer.
48
+
49
+ ## AI Tools
50
+
51
+ The AI can use these tools during conversations:
52
+
53
+ | Tool | Description |
54
+ |------|-------------|
55
+ | `contextualai_list_agents` | List available Contextual AI agents (id, name, description). |
56
+ | `contextualai_query_agent` | Send a message to a specific agent by id and return its response. |
57
+
58
+ ## Optional config
59
+
60
+ | Key | Type | Default | Description |
61
+ |----------|---------|-----------------------------|---------------------------------------------|
62
+ | `baseUrl`| string | `https://api.contextual.ai` | API base URL. |
63
+ | `debug` | boolean | false | Log request URL and response status. |
64
+
65
+ Env vars: `OPENCLAW_CONTEXTUALAI_BASE_URL`, `OPENCLAW_CONTEXTUALAI_API_KEY` (or set `apiKey` in config).
package/biome.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
3
+ "assist": {
4
+ "actions": {
5
+ "source": {
6
+ "organizeImports": "on",
7
+ "useSortedAttributes": "on",
8
+ "useSortedKeys": "off"
9
+ }
10
+ },
11
+ "enabled": true
12
+ },
13
+ "files": {
14
+ "includes": [
15
+ "**",
16
+ "!**/node_modules",
17
+ "!**/dist",
18
+ "!**/bun.lock",
19
+ "!**/*.lock"
20
+ ]
21
+ },
22
+ "formatter": {
23
+ "enabled": true,
24
+ "indentStyle": "tab"
25
+ },
26
+ "javascript": {
27
+ "formatter": {
28
+ "quoteStyle": "double",
29
+ "semicolons": "asNeeded"
30
+ }
31
+ },
32
+ "linter": {
33
+ "domains": {
34
+ "project": "none"
35
+ },
36
+ "enabled": true,
37
+ "rules": {
38
+ "correctness": {
39
+ "useYield": "warn",
40
+ "noUnusedVariables": {
41
+ "level": "warn",
42
+ "options": {
43
+ "ignoreRestSiblings": true
44
+ }
45
+ },
46
+ "noUnusedImports": "warn",
47
+ "useParseIntRadix": "off"
48
+ },
49
+ "recommended": true,
50
+ "style": {
51
+ "noDefaultExport": "off",
52
+ "noInferrableTypes": "error",
53
+ "noNonNullAssertion": "warn",
54
+ "noParameterAssign": "error",
55
+ "noUnusedTemplateLiteral": "error",
56
+ "noUselessElse": "error",
57
+ "useAsConstAssertion": "error",
58
+ "useDefaultParameterLast": "error",
59
+ "useEnumInitializers": "error",
60
+ "useNamingConvention": {
61
+ "level": "off",
62
+ "options": {
63
+ "strictCase": false
64
+ }
65
+ },
66
+ "useNumberNamespace": "error",
67
+ "useSelfClosingElements": "error",
68
+ "useSingleVarDeclarator": "error"
69
+ }
70
+ }
71
+ },
72
+ "vcs": {
73
+ "clientKind": "git",
74
+ "enabled": true,
75
+ "useIgnoreFile": true
76
+ }
77
+ }
package/client.ts ADDED
@@ -0,0 +1,137 @@
1
+ import type { ContextualAiConfig } from "./config.ts"
2
+
3
+ export type AgentEntry = {
4
+ id: string
5
+ name: string
6
+ description: string
7
+ }
8
+
9
+ export type ListAgentsResult = {
10
+ agents: AgentEntry[]
11
+ next_cursor?: string
12
+ }
13
+
14
+ export type MessageRole = {
15
+ role: "user" | "assistant"
16
+ content: string
17
+ }
18
+
19
+ export type QueryAgentResult = {
20
+ content: string
21
+ conversation_id?: string
22
+ message_id?: string
23
+ }
24
+
25
+ function normalizeBaseUrl(baseUrl: string): string {
26
+ const trimmed = baseUrl.replace(/\/+$/, "")
27
+ if (trimmed.endsWith("/v1")) return trimmed
28
+ return `${trimmed}/v1`
29
+ }
30
+
31
+ const API_KEY_REQUIRED_MSG =
32
+ "openclaw-contextualai: apiKey is required (set in plugin config or OPENCLAW_CONTEXTUALAI_API_KEY env var)"
33
+
34
+ export class ContextualAiClient {
35
+ private apiKey: string
36
+ private baseUrl: string
37
+ private debug: boolean
38
+
39
+ constructor(cfg: Pick<ContextualAiConfig, "apiKey" | "baseUrl" | "debug">) {
40
+ this.apiKey = cfg.apiKey ?? ""
41
+ this.baseUrl = normalizeBaseUrl(cfg.baseUrl)
42
+ this.debug = cfg.debug ?? false
43
+ }
44
+
45
+ private ensureApiKey(): void {
46
+ if (!this.apiKey || this.apiKey.length === 0) {
47
+ throw new Error(API_KEY_REQUIRED_MSG)
48
+ }
49
+ }
50
+
51
+ private async request<T>(
52
+ method: string,
53
+ path: string,
54
+ body?: unknown,
55
+ ): Promise<T> {
56
+ const url = `${this.baseUrl}${path}`
57
+ if (this.debug) {
58
+ // eslint-disable-next-line no-console
59
+ console.debug(`[contextualai] ${method} ${url}`)
60
+ }
61
+
62
+ const headers: Record<string, string> = {
63
+ Authorization: `Bearer ${this.apiKey}`,
64
+ }
65
+ if (body !== undefined) {
66
+ headers["Content-Type"] = "application/json"
67
+ }
68
+
69
+ const res = await fetch(url, {
70
+ method,
71
+ headers,
72
+ body: body !== undefined ? JSON.stringify(body) : undefined,
73
+ })
74
+
75
+ if (this.debug) {
76
+ // eslint-disable-next-line no-console
77
+ console.debug(`[contextualai] ${res.status} ${path}`)
78
+ }
79
+
80
+ const text = await res.text()
81
+ if (!res.ok) {
82
+ throw new Error(
83
+ `Contextual AI API error ${res.status}: ${text || res.statusText}`,
84
+ )
85
+ }
86
+
87
+ if (text.length === 0) {
88
+ return undefined as T
89
+ }
90
+ try {
91
+ return JSON.parse(text) as T
92
+ } catch {
93
+ throw new Error(`Contextual AI API invalid JSON: ${text.slice(0, 200)}`)
94
+ }
95
+ }
96
+
97
+ async listAgents(limit = 100, cursor?: string): Promise<ListAgentsResult> {
98
+ this.ensureApiKey()
99
+ const params = new URLSearchParams()
100
+ params.set("limit", String(limit))
101
+ if (cursor) params.set("cursor", cursor)
102
+ const path = `/agents?${params.toString()}`
103
+ const data = await this.request<{ next_cursor?: string; agents?: AgentEntry[] }>(
104
+ "GET",
105
+ path,
106
+ )
107
+ return {
108
+ agents: data.agents ?? [],
109
+ next_cursor: data.next_cursor,
110
+ }
111
+ }
112
+
113
+ async queryAgent(
114
+ agentId: string,
115
+ messages: MessageRole[],
116
+ conversationId?: string,
117
+ ): Promise<QueryAgentResult> {
118
+ this.ensureApiKey()
119
+ const body: { messages: MessageRole[]; conversation_id?: string } = {
120
+ messages,
121
+ }
122
+ if (conversationId) body.conversation_id = conversationId
123
+
124
+ const path = `/agents/${encodeURIComponent(agentId)}/query`
125
+ const data = await this.request<{
126
+ conversation_id?: string
127
+ message_id?: string
128
+ message?: { content?: string; role?: string }
129
+ }>("POST", path, body)
130
+
131
+ return {
132
+ content: data.message?.content ?? "",
133
+ conversation_id: data.conversation_id,
134
+ message_id: data.message_id,
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,37 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import type { ContextualAiConfig } from "../config.ts"
3
+
4
+ export function registerCommands(
5
+ api: OpenClawPluginApi,
6
+ _cfg: ContextualAiConfig,
7
+ ): void {
8
+ api.registerCommand({
9
+ name: "contextai",
10
+ description: "Inspect or test Contextual AI configuration and behavior.",
11
+ acceptsArgs: true,
12
+ requireAuth: true,
13
+ async handler(ctx: { args?: string }) {
14
+ const subcommand = ctx.args?.trim() || "help"
15
+
16
+ if (subcommand === "help") {
17
+ return {
18
+ text:
19
+ "/contextai commands:\n" +
20
+ "- /contextai help Show this help.\n" +
21
+ "- /contextai ping Check that the plugin is wired correctly.\n",
22
+ }
23
+ }
24
+
25
+ if (subcommand === "ping") {
26
+ return {
27
+ text: "openclaw-contextualai: plugin is installed and responding.",
28
+ }
29
+ }
30
+
31
+ return {
32
+ text: `Unknown subcommand: "${subcommand}". Try /contextai help.`,
33
+ }
34
+ },
35
+ })
36
+ }
37
+
package/config.ts ADDED
@@ -0,0 +1,78 @@
1
+ export type ContextualAiConfig = {
2
+ apiKey: string
3
+ baseUrl: string
4
+ projectId?: string
5
+ autoInject: boolean
6
+ maxContextTokens: number
7
+ debug: boolean
8
+ }
9
+
10
+ const ALLOWED_KEYS = [
11
+ "apiKey",
12
+ "baseUrl",
13
+ "projectId",
14
+ "autoInject",
15
+ "maxContextTokens",
16
+ "debug",
17
+ ] as const
18
+
19
+ function assertAllowedKeys(
20
+ value: Record<string, unknown>,
21
+ allowed: readonly string[],
22
+ label: string,
23
+ ): void {
24
+ const unknown = Object.keys(value).filter((k) => !allowed.includes(k))
25
+ if (unknown.length > 0) {
26
+ throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`)
27
+ }
28
+ }
29
+
30
+ function resolveEnvVars(value: string): string {
31
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar: string) => {
32
+ const envValue = process.env[envVar]
33
+ if (!envValue) {
34
+ throw new Error(`Environment variable ${envVar} is not set`)
35
+ }
36
+ return envValue
37
+ })
38
+ }
39
+
40
+ export function parseConfig(raw: unknown): ContextualAiConfig {
41
+ const cfg =
42
+ raw && typeof raw === "object" && !Array.isArray(raw)
43
+ ? (raw as Record<string, unknown>)
44
+ : {}
45
+
46
+ if (Object.keys(cfg).length > 0) {
47
+ assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-contextualai config")
48
+ }
49
+
50
+ const apiKeyValue = cfg.apiKey ?? process.env.OPENCLAW_CONTEXTUALAI_API_KEY
51
+ const baseUrlValue =
52
+ cfg.baseUrl ?? process.env.OPENCLAW_CONTEXTUALAI_BASE_URL ?? "https://api.contextual.ai"
53
+
54
+ // Allow missing key at startup so the gateway can start; the client will throw when a tool is used.
55
+ const apiKey =
56
+ apiKeyValue && typeof apiKeyValue === "string" && apiKeyValue.length > 0
57
+ ? resolveEnvVars(apiKeyValue)
58
+ : ""
59
+
60
+ const baseUrl =
61
+ typeof baseUrlValue === "string" && baseUrlValue.length > 0
62
+ ? resolveEnvVars(baseUrlValue)
63
+ : "https://api.contextual.ai"
64
+
65
+ return {
66
+ apiKey,
67
+ baseUrl,
68
+ projectId: typeof cfg.projectId === "string" ? cfg.projectId : undefined,
69
+ autoInject: (cfg.autoInject as boolean) ?? true,
70
+ maxContextTokens: (cfg.maxContextTokens as number) ?? 4096,
71
+ debug: (cfg.debug as boolean) ?? false,
72
+ }
73
+ }
74
+
75
+ export const contextualAiConfigSchema = {
76
+ parse: parseConfig,
77
+ }
78
+
@@ -0,0 +1,23 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import type { ContextualAiConfig } from "../config.ts"
3
+
4
+ export function registerBeforeAgentStartHook(
5
+ api: OpenClawPluginApi,
6
+ _cfg: ContextualAiConfig,
7
+ ): void {
8
+ api.on(
9
+ "before_agent_start",
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ async (event: Record<string, any>, _ctx: Record<string, any>) => {
12
+ // TODO: use your contextual backend + event/ctx to fetch relevant context.
13
+ // For now, we just return the original event without modifications.
14
+
15
+ // Example of how you might eventually inject context:
16
+ // const extraContext = await fetchContextFromBackend(event, ctx, cfg)
17
+ // return { ...event, prependContext: extraContext }
18
+
19
+ return event
20
+ },
21
+ )
22
+ }
23
+
package/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import { ContextualAiClient } from "./client.ts"
3
+ import { parseConfig, contextualAiConfigSchema } from "./config.ts"
4
+ import { registerBeforeAgentStartHook } from "./hooks/before_agent_start.ts"
5
+ import { registerCommands } from "./commands/slash.ts"
6
+ import { registerListAgentsTool } from "./tools/contextualai_list_agents.ts"
7
+ import { registerQueryAgentTool } from "./tools/contextualai_query_agent.ts"
8
+ import { registerContextualLookupTool } from "./tools/contextualai_lookup.ts"
9
+
10
+ export default {
11
+ id: "openclaw-contextualai",
12
+ name: "Contextual AI",
13
+ description: "Context-aware extension for OpenClaw that injects relevant context into conversations.",
14
+ kind: "extension" as const,
15
+ configSchema: contextualAiConfigSchema,
16
+
17
+ register(api: OpenClawPluginApi) {
18
+ const cfg = parseConfig(api.pluginConfig)
19
+ const client = new ContextualAiClient(cfg)
20
+
21
+ // Register tools: list agents and query agent (Step 1 – talk to Contextual AI agents directly).
22
+ registerListAgentsTool(api, client, cfg)
23
+ registerQueryAgentTool(api, client, cfg)
24
+ registerContextualLookupTool(api, cfg)
25
+
26
+ // Register hooks to inject context before agent runs.
27
+ registerBeforeAgentStartHook(api, cfg)
28
+
29
+ // Register basic slash commands for debugging / inspection.
30
+ registerCommands(api, cfg)
31
+
32
+ api.registerService({
33
+ id: "openclaw-contextualai",
34
+ start: () => {
35
+ api.logger.info("openclaw-contextualai: connected")
36
+ },
37
+ stop: () => {
38
+ api.logger.info("openclaw-contextualai: stopped")
39
+ },
40
+ })
41
+ },
42
+ }
43
+
@@ -0,0 +1,53 @@
1
+ {
2
+ "id": "openclaw-contextualai",
3
+ "kind": "extension",
4
+ "uiHints": {
5
+ "apiKey": {
6
+ "label": "Contextual AI API Key",
7
+ "sensitive": true,
8
+ "placeholder": "${OPENCLAW_CONTEXTUALAI_API_KEY}",
9
+ "help": "API key for your Contextual AI backend. You can also set it via the OPENCLAW_CONTEXTUALAI_API_KEY environment variable."
10
+ },
11
+ "baseUrl": {
12
+ "label": "Base URL",
13
+ "placeholder": "https://api.contextual.ai",
14
+ "help": "Base URL for the Contextual AI service.",
15
+ "advanced": true
16
+ },
17
+ "projectId": {
18
+ "label": "Project ID",
19
+ "placeholder": "optional-project-id",
20
+ "help": "Optional project or workspace identifier used by your contextual backend.",
21
+ "advanced": true
22
+ },
23
+ "autoInject": {
24
+ "label": "Auto Inject Context",
25
+ "help": "Automatically fetch and inject relevant contextual information before every AI turn."
26
+ },
27
+ "maxContextTokens": {
28
+ "label": "Max Context Tokens",
29
+ "placeholder": "4096",
30
+ "help": "Maximum number of tokens of contextual information to inject into each turn.",
31
+ "advanced": true
32
+ },
33
+ "debug": {
34
+ "label": "Debug Logging",
35
+ "help": "Enable verbose debug logs for contextual requests and responses.",
36
+ "advanced": true
37
+ }
38
+ },
39
+ "configSchema": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "apiKey": { "type": "string" },
44
+ "baseUrl": { "type": "string" },
45
+ "projectId": { "type": "string" },
46
+ "autoInject": { "type": "boolean" },
47
+ "maxContextTokens": { "type": "number", "minimum": 512, "maximum": 32768 },
48
+ "debug": { "type": "boolean" }
49
+ },
50
+ "required": []
51
+ }
52
+ }
53
+
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "openclaw-contextualai",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Contextual AI extension for OpenClaw",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "check-types": "tsc --noEmit",
9
+ "lint": "bunx @biomejs/biome ci .",
10
+ "lint:fix": "bunx @biomejs/biome check --write ."
11
+ },
12
+ "peerDependencies": {
13
+ "openclaw": ">=2026.1.29"
14
+ },
15
+ "openclaw": {
16
+ "extensions": [
17
+ "./index.ts"
18
+ ]
19
+ },
20
+ "devDependencies": {
21
+ "@sinclair/typebox": "0.34.47",
22
+ "typescript": "^5.9.3"
23
+ }
24
+ }
@@ -0,0 +1,62 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { ContextualAiClient } from "../client.ts"
4
+ import type { ContextualAiConfig } from "../config.ts"
5
+
6
+ export function registerListAgentsTool(
7
+ api: OpenClawPluginApi,
8
+ client: ContextualAiClient,
9
+ _cfg: ContextualAiConfig,
10
+ ): void {
11
+ api.registerTool(
12
+ {
13
+ name: "contextualai_list_agents",
14
+ label: "List Contextual AI Agents",
15
+ description:
16
+ "Returns the list of Contextual AI agents available to query. Use this to see agent id, name, and description so you can choose the right agent and call contextualai_query_agent with its id.",
17
+ parameters: Type.Object({
18
+ limit: Type.Optional(
19
+ Type.Number({
20
+ description: "Maximum number of agents to return (default 50)",
21
+ default: 50,
22
+ }),
23
+ ),
24
+ }),
25
+ async execute(_toolCallId: string, params: { limit?: number }) {
26
+ const limit = params.limit ?? 50
27
+ const result = await client.listAgents(limit)
28
+
29
+ if (result.agents.length === 0) {
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text" as const,
34
+ text: "No Contextual AI agents found.",
35
+ },
36
+ ],
37
+ details: { agents: [], count: 0 },
38
+ }
39
+ }
40
+
41
+ const lines = result.agents.map(
42
+ (a) => `- **${a.name}** (id: \`${a.id}\`): ${a.description ?? ""}`,
43
+ )
44
+ const text =
45
+ `Found ${result.agents.length} agent(s):\n\n${lines.join("\n")}` +
46
+ (result.next_cursor
47
+ ? "\n\n(More agents available; use limit to fetch more.)"
48
+ : "")
49
+
50
+ return {
51
+ content: [{ type: "text" as const, text }],
52
+ details: {
53
+ agents: result.agents,
54
+ count: result.agents.length,
55
+ next_cursor: result.next_cursor,
56
+ },
57
+ }
58
+ },
59
+ },
60
+ { name: "contextualai_list_agents" },
61
+ )
62
+ }
@@ -0,0 +1,44 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { ContextualAiConfig } from "../config.ts"
4
+
5
+ export function registerContextualLookupTool(
6
+ api: OpenClawPluginApi,
7
+ _cfg: ContextualAiConfig,
8
+ ): void {
9
+ api.registerTool(
10
+ {
11
+ name: "contextualai_lookup",
12
+ label: "Contextual Lookup",
13
+ description:
14
+ "Fetches contextual information relevant to the current conversation or query.",
15
+ parameters: Type.Object({
16
+ query: Type.String({
17
+ description:
18
+ "Natural language description of the context the AI wants to retrieve.",
19
+ }),
20
+ }),
21
+ // Note: this is a stub implementation – wire it to your backend later.
22
+ async execute(_toolCallId: string, params: { query: string }) {
23
+ const preview =
24
+ params.query.length > 120
25
+ ? `${params.query.slice(0, 120)}…`
26
+ : params.query
27
+
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text" as const,
32
+ text:
33
+ `[contextualai_lookup] This is a placeholder response for query: "${preview}". ` +
34
+ "Wire this tool up to your contextual backend to return real context snippets.",
35
+ },
36
+ ],
37
+ details: {},
38
+ }
39
+ },
40
+ },
41
+ { name: "contextualai_lookup" },
42
+ )
43
+ }
44
+
@@ -0,0 +1,81 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { ContextualAiClient } from "../client.ts"
4
+ import type { ContextualAiConfig } from "../config.ts"
5
+
6
+ export function registerQueryAgentTool(
7
+ api: OpenClawPluginApi,
8
+ client: ContextualAiClient,
9
+ _cfg: ContextualAiConfig,
10
+ ): void {
11
+ api.registerTool(
12
+ {
13
+ name: "contextualai_query_agent",
14
+ label: "Query Contextual AI Agent",
15
+ description:
16
+ "Send a message to a specific Contextual AI agent by its id (from contextualai_list_agents) and get the agent's response. Use this to redirect user questions to the appropriate specialist agent.",
17
+ parameters: Type.Object({
18
+ agent_id: Type.String({
19
+ description: "UUID of the agent to query (from contextualai_list_agents)",
20
+ }),
21
+ message: Type.String({
22
+ description: "The user message or question to send to the agent",
23
+ }),
24
+ conversation_id: Type.Optional(
25
+ Type.String({
26
+ description:
27
+ "Optional conversation id for follow-up messages in the same thread",
28
+ }),
29
+ ),
30
+ }),
31
+ async execute(
32
+ _toolCallId: string,
33
+ params: { agent_id: string; message: string; conversation_id?: string },
34
+ ) {
35
+ try {
36
+ const result = await client.queryAgent(
37
+ params.agent_id,
38
+ [{ role: "user", content: params.message }],
39
+ params.conversation_id,
40
+ )
41
+
42
+ if (!result.content) {
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text" as const,
47
+ text: "The agent returned an empty response.",
48
+ },
49
+ ],
50
+ details: {
51
+ conversation_id: result.conversation_id,
52
+ message_id: result.message_id,
53
+ },
54
+ }
55
+ }
56
+
57
+ return {
58
+ content: [{ type: "text" as const, text: result.content }],
59
+ details: {
60
+ conversation_id: result.conversation_id,
61
+ message_id: result.message_id,
62
+ },
63
+ }
64
+ } catch (err) {
65
+ const message =
66
+ err instanceof Error ? err.message : String(err)
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text" as const,
71
+ text: `Contextual AI query failed: ${message}`,
72
+ },
73
+ ],
74
+ details: {},
75
+ }
76
+ }
77
+ },
78
+ },
79
+ { name: "contextualai_query_agent" },
80
+ )
81
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "allowImportingTsExtensions": true,
12
+ "noEmit": true,
13
+ "rootDir": "."
14
+ },
15
+ "include": [
16
+ "*.ts",
17
+ "tools/*.ts",
18
+ "hooks/*.ts",
19
+ "commands/*.ts",
20
+ "types/*.d.ts"
21
+ ],
22
+ "exclude": ["node_modules", "dist"]
23
+ }