@meetopenbot/codex 1.0.11 → 1.1.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.
- package/README.md +16 -30
- package/dist/index.d.ts +4 -0
- package/dist/index.js +219 -83
- package/package.json +16 -10
- package/assets/icon.svg +0 -1
package/README.md
CHANGED
|
@@ -1,37 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @meetopenbot/codex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenAI Codex runtime plugin for OpenBot. Each `agent:invoke` starts or resumes a Codex SDK thread in the channel workspace.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Config
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- One tool definition: `codex_run`
|
|
9
|
-
- One action handler: `action:codex_run`
|
|
10
|
-
- `@openai/codex-sdk` integration via `Codex` + `thread.run(...)`
|
|
7
|
+
Set an API key with one of:
|
|
11
8
|
|
|
12
|
-
## Local usage
|
|
13
|
-
|
|
14
|
-
1. Install dependencies:
|
|
15
|
-
|
|
16
|
-
`npm install`
|
|
17
|
-
|
|
18
|
-
2. Build:
|
|
19
|
-
|
|
20
|
-
`npm run build`
|
|
21
|
-
|
|
22
|
-
3. Load `dist/index.js` from your OpenBot plugin registry/runtime.
|
|
23
|
-
|
|
24
|
-
## Runtime config
|
|
25
|
-
|
|
26
|
-
Set an API key using one of the following:
|
|
27
|
-
|
|
28
|
-
- `CODEX_API_KEY` environment variable
|
|
29
|
-
- `OPENAI_API_KEY` environment variable
|
|
30
9
|
- `apiKey` in plugin options
|
|
10
|
+
- `CODEX_API_KEY`
|
|
11
|
+
- `OPENAI_API_KEY`
|
|
12
|
+
|
|
13
|
+
Optional:
|
|
31
14
|
|
|
32
|
-
|
|
15
|
+
- `model` — Codex model id (e.g. `gpt-5.6`). Omit to use the CLI default.
|
|
16
|
+
- `sandboxMode` — `read-only` | `workspace-write` | `danger-full-access` (default `workspace-write`)
|
|
17
|
+
- `approvalPolicy` — `never` | `on-request` | `on-failure` | `untrusted` (default `never`)
|
|
18
|
+
- `workingDirectory` — override the channel cwd
|
|
19
|
+
- `skipGitRepoCheck` — default `true` for non-git channel workspaces
|
|
20
|
+
- `baseURL` — custom OpenAI-compatible endpoint
|
|
21
|
+
- `codexPathOverride` — path to a local `codex` binary (`CODEX_PATH` / `CODEX_CLI_PATH` also work)
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
- `baseURL` (for custom OpenAI-compatible endpoints)
|
|
36
|
-
- `workingDirectory`, `skipGitRepoCheck`, `sandboxMode`, `approvalPolicy`
|
|
37
|
-
- `networkAccessEnabled`, `webSearchMode`, `threadId`
|
|
23
|
+
Codex conversation state is stored as `codexThreadId` on the OpenBot thread, not as the OpenBot thread id.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -2,139 +2,275 @@
|
|
|
2
2
|
import {
|
|
3
3
|
definePlugin,
|
|
4
4
|
shouldHandleInvoke,
|
|
5
|
-
agentOutput
|
|
5
|
+
agentOutput,
|
|
6
|
+
uiWidget
|
|
6
7
|
} from "@meetopenbot/plugin-sdk";
|
|
7
8
|
import {
|
|
8
9
|
Codex
|
|
9
10
|
} from "@openai/codex-sdk";
|
|
10
|
-
var
|
|
11
|
+
var SANDBOX_MODES = [
|
|
12
|
+
"read-only",
|
|
13
|
+
"workspace-write",
|
|
14
|
+
"danger-full-access"
|
|
15
|
+
];
|
|
16
|
+
var APPROVAL_MODES = [
|
|
17
|
+
"never",
|
|
18
|
+
"on-request",
|
|
19
|
+
"on-failure",
|
|
20
|
+
"untrusted"
|
|
21
|
+
];
|
|
22
|
+
var LEGACY_SANDBOX = {
|
|
23
|
+
"workspace-read": "read-only",
|
|
24
|
+
"full-read": "read-only",
|
|
25
|
+
"full-write": "danger-full-access"
|
|
26
|
+
};
|
|
27
|
+
var LEGACY_APPROVAL = {
|
|
28
|
+
always: "on-request",
|
|
29
|
+
automatic: "on-request"
|
|
30
|
+
};
|
|
31
|
+
var AUTH_ERROR_PATTERNS = [
|
|
32
|
+
"api key",
|
|
33
|
+
"apikey",
|
|
34
|
+
"unauthorized",
|
|
35
|
+
"401",
|
|
36
|
+
"authentication",
|
|
37
|
+
"not logged in",
|
|
38
|
+
"login"
|
|
39
|
+
];
|
|
40
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
41
|
+
var asString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
42
|
+
var isAuthErrorMessage = (message) => {
|
|
43
|
+
const lower = message.toLowerCase();
|
|
44
|
+
return AUTH_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));
|
|
45
|
+
};
|
|
46
|
+
var readPersistedThreadId = (state) => {
|
|
47
|
+
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
48
|
+
const record = asRecord(source);
|
|
49
|
+
return asString(record.codexThreadId);
|
|
50
|
+
};
|
|
51
|
+
var persistThreadId = async (state, storage, codexThreadId) => {
|
|
52
|
+
if (!storage || !state.channelId) return;
|
|
53
|
+
const patch = { codexThreadId };
|
|
54
|
+
if (state.threadId) {
|
|
55
|
+
await storage.patchThreadState({
|
|
56
|
+
channelId: state.channelId,
|
|
57
|
+
threadId: state.threadId,
|
|
58
|
+
state: patch
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await storage.patchChannelState({ channelId: state.channelId, state: patch });
|
|
63
|
+
};
|
|
64
|
+
var parseSandboxMode = (value) => {
|
|
65
|
+
const raw = asString(value);
|
|
66
|
+
if (!raw) return "workspace-write";
|
|
67
|
+
if (SANDBOX_MODES.includes(raw)) return raw;
|
|
68
|
+
return LEGACY_SANDBOX[raw] ?? "workspace-write";
|
|
69
|
+
};
|
|
70
|
+
var parseApprovalPolicy = (value) => {
|
|
71
|
+
const raw = asString(value);
|
|
72
|
+
if (!raw) return "never";
|
|
73
|
+
if (APPROVAL_MODES.includes(raw)) return raw;
|
|
74
|
+
return LEGACY_APPROVAL[raw] ?? "never";
|
|
75
|
+
};
|
|
76
|
+
var parseModel = (value) => {
|
|
77
|
+
const raw = asString(value);
|
|
78
|
+
if (!raw) return void 0;
|
|
79
|
+
return raw.split("/").pop() || void 0;
|
|
80
|
+
};
|
|
81
|
+
var itemWidget = (item) => {
|
|
82
|
+
switch (item.type) {
|
|
83
|
+
case "command_execution":
|
|
84
|
+
return { title: "Command", body: item.command };
|
|
85
|
+
case "file_change":
|
|
86
|
+
return {
|
|
87
|
+
title: "File change",
|
|
88
|
+
body: item.changes.map((change) => `${change.kind} ${change.path}`).join("\n")
|
|
89
|
+
};
|
|
90
|
+
case "error":
|
|
91
|
+
return { title: "Error", body: item.message };
|
|
92
|
+
default:
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var errorText = (error) => error instanceof Error ? error.message : "Codex request failed.";
|
|
97
|
+
var buildApiKeyWidget = (agentId, threadId, reason) => uiWidget({
|
|
98
|
+
agentId,
|
|
99
|
+
threadId,
|
|
100
|
+
widget: {
|
|
101
|
+
kind: "form",
|
|
102
|
+
widgetId: `codex_api_key_request_${Date.now()}`,
|
|
103
|
+
title: "OpenAI API Key Required",
|
|
104
|
+
description: `Codex could not authenticate (${reason}). Provide an OpenAI API key to continue. You can get one from the OpenAI dashboard. The key is stored as a workspace variable on your machine and never leaves your local runtime.`,
|
|
105
|
+
fields: [
|
|
106
|
+
{
|
|
107
|
+
id: "apiKey",
|
|
108
|
+
label: "API Key",
|
|
109
|
+
type: "password",
|
|
110
|
+
placeholder: "sk-...",
|
|
111
|
+
required: true
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
submitLabel: "Save API Key",
|
|
115
|
+
metadata: {
|
|
116
|
+
type: "api_key_request",
|
|
117
|
+
provider: "openai",
|
|
118
|
+
envVar: "OPENAI_API_KEY",
|
|
119
|
+
source: "codex"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
var plugin = definePlugin({
|
|
11
124
|
id: "codex",
|
|
12
125
|
name: "Codex",
|
|
13
|
-
description: "Codex
|
|
126
|
+
description: "OpenAI Codex agent. Uses the Codex SDK to read code, edit files, and run shell commands inside the channel's workspace.",
|
|
14
127
|
configSchema: {
|
|
15
128
|
type: "object",
|
|
16
129
|
properties: {
|
|
17
|
-
apiKey: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
130
|
+
apiKey: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "OpenAI API key (falls back to CODEX_API_KEY / OPENAI_API_KEY)",
|
|
133
|
+
format: "password"
|
|
134
|
+
},
|
|
135
|
+
model: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Codex model id (e.g. gpt-5.6). Leave empty to use the CLI default."
|
|
138
|
+
},
|
|
23
139
|
sandboxMode: {
|
|
24
140
|
type: "string",
|
|
25
|
-
enum: [
|
|
26
|
-
description: "
|
|
141
|
+
enum: [...SANDBOX_MODES],
|
|
142
|
+
description: "Filesystem sandbox: read-only | workspace-write | danger-full-access",
|
|
27
143
|
default: "workspace-write"
|
|
28
144
|
},
|
|
29
145
|
approvalPolicy: {
|
|
30
146
|
type: "string",
|
|
31
|
-
enum: [
|
|
32
|
-
description: "
|
|
147
|
+
enum: [...APPROVAL_MODES],
|
|
148
|
+
description: "When to require approval: never | on-request | on-failure | untrusted",
|
|
33
149
|
default: "never"
|
|
34
150
|
},
|
|
35
|
-
|
|
36
|
-
webSearchMode: {
|
|
151
|
+
workingDirectory: {
|
|
37
152
|
type: "string",
|
|
38
|
-
|
|
39
|
-
|
|
153
|
+
description: "Override the channel working directory"
|
|
154
|
+
},
|
|
155
|
+
skipGitRepoCheck: {
|
|
156
|
+
type: "boolean",
|
|
157
|
+
description: "Skip the Codex git-repo check (needed for non-git channel workspaces)",
|
|
158
|
+
default: true
|
|
159
|
+
},
|
|
160
|
+
baseURL: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Custom OpenAI-compatible endpoint"
|
|
163
|
+
},
|
|
164
|
+
codexPathOverride: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Path to a local Codex CLI binary"
|
|
40
167
|
}
|
|
41
168
|
}
|
|
42
169
|
},
|
|
43
170
|
factory: (context) => {
|
|
44
171
|
const config = context.config;
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
172
|
+
const apiKey = asString(config.apiKey) ?? process.env.CODEX_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
173
|
+
const codexPathOverride = asString(config.codexPathOverride) ?? process.env.CODEX_PATH ?? process.env.CODEX_CLI_PATH;
|
|
174
|
+
const model = parseModel(config.model);
|
|
175
|
+
const sandboxMode = parseSandboxMode(config.sandboxMode);
|
|
176
|
+
const approvalPolicy = parseApprovalPolicy(config.approvalPolicy);
|
|
177
|
+
const skipGitRepoCheck = config.skipGitRepoCheck !== false;
|
|
178
|
+
const workingDirectoryOverride = asString(config.workingDirectory);
|
|
179
|
+
const baseUrl = asString(config.baseURL);
|
|
49
180
|
let client;
|
|
50
181
|
const getClient = () => {
|
|
51
182
|
if (!client) {
|
|
52
183
|
client = new Codex({
|
|
53
|
-
apiKey,
|
|
184
|
+
...apiKey && { apiKey },
|
|
54
185
|
...codexPathOverride && { codexPathOverride },
|
|
55
|
-
...
|
|
186
|
+
...baseUrl && { baseUrl }
|
|
56
187
|
});
|
|
57
188
|
}
|
|
58
189
|
return client;
|
|
59
190
|
};
|
|
60
|
-
let thread = null;
|
|
61
|
-
const getThread = (state, meta) => {
|
|
62
|
-
if (thread) return thread;
|
|
63
|
-
const workingDirectory = config.workingDirectory || state?.channelDetails?.cwd || globalThis?.process?.cwd() || "/tmp";
|
|
64
|
-
const threadOptions = {
|
|
65
|
-
model,
|
|
66
|
-
workingDirectory,
|
|
67
|
-
skipGitRepoCheck: config.skipGitRepoCheck ?? true,
|
|
68
|
-
sandboxMode: config.sandboxMode ?? "workspace-write",
|
|
69
|
-
approvalPolicy: config.approvalPolicy ?? "never",
|
|
70
|
-
...typeof config.networkAccessEnabled === "boolean" && {
|
|
71
|
-
networkAccessEnabled: config.networkAccessEnabled
|
|
72
|
-
},
|
|
73
|
-
...config.webSearchMode && {
|
|
74
|
-
webSearchMode: config.webSearchMode
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
const threadId = state?.threadId || meta?.threadId;
|
|
78
|
-
thread = threadId ? getClient().resumeThread(threadId, threadOptions) : getClient().startThread(threadOptions);
|
|
79
|
-
if (!threadId && state) {
|
|
80
|
-
state.threadId = thread.id;
|
|
81
|
-
}
|
|
82
|
-
return thread;
|
|
83
|
-
};
|
|
84
191
|
return (builder) => {
|
|
85
192
|
builder.on("agent:invoke", async function* (event, ctx) {
|
|
86
193
|
if (!shouldHandleInvoke(event, context.agentId)) return;
|
|
87
|
-
const
|
|
194
|
+
const content = asString(event.data?.content);
|
|
195
|
+
const openbotThreadId = event.meta?.threadId || ctx.state.threadId;
|
|
88
196
|
if (!content) {
|
|
89
197
|
yield agentOutput({
|
|
90
198
|
agentId: context.agentId,
|
|
91
199
|
content: "No content provided.",
|
|
92
|
-
threadId:
|
|
200
|
+
threadId: openbotThreadId
|
|
93
201
|
});
|
|
94
202
|
return;
|
|
95
203
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} else if (chunk.item.type === "error") {
|
|
110
|
-
outputContent = `Error: ${chunk.item.message}`;
|
|
111
|
-
} else if (chunk.item.type === "todo_list") {
|
|
112
|
-
outputContent = chunk.item.items.map(
|
|
113
|
-
(item) => `- ${item.text} (${item.completed ? "completed" : "pending"})`
|
|
114
|
-
).join("\n");
|
|
115
|
-
} else if (chunk.item.type === "web_search") {
|
|
116
|
-
outputContent = `Searching: ${chunk.item.query}`;
|
|
117
|
-
}
|
|
118
|
-
if (outputContent) {
|
|
119
|
-
yield agentOutput({
|
|
120
|
-
agentId: context.agentId,
|
|
121
|
-
content: outputContent,
|
|
122
|
-
threadId: event.meta?.threadId
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
204
|
+
const workingDirectory = workingDirectoryOverride || ctx.state.channelDetails?.cwd || process.cwd();
|
|
205
|
+
const threadOptions = {
|
|
206
|
+
workingDirectory,
|
|
207
|
+
skipGitRepoCheck,
|
|
208
|
+
sandboxMode,
|
|
209
|
+
approvalPolicy,
|
|
210
|
+
...model && { model }
|
|
211
|
+
};
|
|
212
|
+
const savedId = readPersistedThreadId(ctx.state);
|
|
213
|
+
const thread = savedId ? getClient().resumeThread(savedId, threadOptions) : getClient().startThread(threadOptions);
|
|
214
|
+
const fail = function* (message) {
|
|
215
|
+
if (isAuthErrorMessage(message)) {
|
|
216
|
+
yield buildApiKeyWidget(context.agentId, openbotThreadId, message);
|
|
126
217
|
}
|
|
127
|
-
} catch (error) {
|
|
128
218
|
yield agentOutput({
|
|
129
219
|
agentId: context.agentId,
|
|
130
|
-
content: `Error: ${
|
|
131
|
-
threadId:
|
|
220
|
+
content: `Error: ${message}`,
|
|
221
|
+
threadId: openbotThreadId
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
225
|
+
const { events } = await thread.runStreamed(content, {
|
|
226
|
+
signal: context.abortSignal
|
|
132
227
|
});
|
|
228
|
+
for await (const chunk of events) {
|
|
229
|
+
if (chunk.type === "thread.started") {
|
|
230
|
+
await persistThreadId(ctx.state, context.storage, chunk.thread_id);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (chunk.type === "turn.failed") {
|
|
234
|
+
yield* fail(chunk.error.message);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (chunk.type === "error") {
|
|
238
|
+
yield* fail(chunk.message);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (chunk.type !== "item.completed") continue;
|
|
242
|
+
if (chunk.item.type === "agent_message") {
|
|
243
|
+
yield agentOutput({
|
|
244
|
+
agentId: context.agentId,
|
|
245
|
+
content: chunk.item.text,
|
|
246
|
+
threadId: openbotThreadId
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const widget = itemWidget(chunk.item);
|
|
251
|
+
if (!widget) continue;
|
|
252
|
+
yield uiWidget({
|
|
253
|
+
agentId: context.agentId,
|
|
254
|
+
threadId: openbotThreadId,
|
|
255
|
+
widget: {
|
|
256
|
+
kind: "message",
|
|
257
|
+
widgetId: `codex_${chunk.item.id}`,
|
|
258
|
+
title: widget.title,
|
|
259
|
+
body: widget.body,
|
|
260
|
+
variant: "basic",
|
|
261
|
+
display: "collapsed"
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
yield* fail(errorText(error));
|
|
133
267
|
}
|
|
134
268
|
});
|
|
135
269
|
};
|
|
136
270
|
}
|
|
137
271
|
});
|
|
272
|
+
var plugin_codex_default = plugin;
|
|
138
273
|
export {
|
|
139
|
-
|
|
274
|
+
plugin_codex_default as default,
|
|
275
|
+
plugin
|
|
140
276
|
};
|
package/package.json
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/codex",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Codex
|
|
5
|
+
"description": "OpenAI Codex agent plugin for OpenBot",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
|
-
".":
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
12
16
|
},
|
|
13
17
|
"files": [
|
|
14
|
-
"dist"
|
|
15
|
-
"assets"
|
|
18
|
+
"dist"
|
|
16
19
|
],
|
|
17
20
|
"scripts": {
|
|
18
|
-
"build": "esbuild index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@meetopenbot/plugin-sdk --external:@openai/codex-sdk --external:zod",
|
|
19
|
-
"
|
|
21
|
+
"build": "esbuild index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@meetopenbot/plugin-sdk --external:@openai/codex-sdk --external:zod && node ../../scripts/write-plugin-declaration.mjs",
|
|
22
|
+
"dev": "esbuild index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@meetopenbot/plugin-sdk --external:@openai/codex-sdk --external:zod --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit --allowImportingTsExtensions --module ESNext --moduleResolution Bundler --target ES2022 --skipLibCheck index.ts",
|
|
24
|
+
"prepack": "pnpm build"
|
|
20
25
|
},
|
|
21
26
|
"dependencies": {
|
|
22
|
-
"@meetopenbot/plugin-sdk": "
|
|
23
|
-
"@openai/codex-sdk": "
|
|
27
|
+
"@meetopenbot/plugin-sdk": "workspace:^",
|
|
28
|
+
"@openai/codex-sdk": "0.148.0"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
26
31
|
"@types/node": "^25.6.0",
|
|
27
32
|
"esbuild": "^0.21.0",
|
|
28
33
|
"zod": "^4.4.3"
|
|
29
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"types": "./dist/index.d.ts"
|
|
30
36
|
}
|
package/assets/icon.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path d="M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z" fill="url(#lobe-icons-codex-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-_R_0_" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg>
|