@intent-systems/nexus 2026.1.5-3 → 2026.1.5-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.
- package/dist/capabilities/detector.js +214 -0
- package/dist/capabilities/registry.js +98 -0
- package/dist/channels/location.js +44 -0
- package/dist/channels/web/index.js +2 -0
- package/dist/control-plane/broker/broker.js +969 -0
- package/dist/control-plane/compaction.js +284 -0
- package/dist/control-plane/factory.js +31 -0
- package/dist/control-plane/index.js +10 -0
- package/dist/control-plane/odu/agents.js +187 -0
- package/dist/control-plane/odu/interaction-tools.js +196 -0
- package/dist/control-plane/odu/prompt-loader.js +95 -0
- package/dist/control-plane/odu/runtime.js +467 -0
- package/dist/control-plane/odu/types.js +6 -0
- package/dist/control-plane/odu-control-plane.js +314 -0
- package/dist/control-plane/single-agent.js +249 -0
- package/dist/control-plane/types.js +11 -0
- package/dist/credentials/store.js +323 -0
- package/dist/logging/redact.js +109 -0
- package/dist/markdown/fences.js +58 -0
- package/dist/memory/embeddings.js +146 -0
- package/dist/memory/index.js +382 -0
- package/dist/memory/internal.js +163 -0
- package/dist/pairing/pairing-store.js +194 -0
- package/dist/plugins/cli.js +42 -0
- package/dist/plugins/discovery.js +253 -0
- package/dist/plugins/install.js +181 -0
- package/dist/plugins/loader.js +290 -0
- package/dist/plugins/registry.js +105 -0
- package/dist/plugins/status.js +29 -0
- package/dist/plugins/tools.js +39 -0
- package/dist/plugins/types.js +1 -0
- package/dist/routing/resolve-route.js +144 -0
- package/dist/routing/session-key.js +63 -0
- package/dist/utils/provider-utils.js +28 -0
- package/package.json +4 -29
- package/patches/@mariozechner__pi-ai.patch +215 -0
- package/patches/playwright-core@1.57.0.patch +13 -0
- package/patches/qrcode-terminal.patch +12 -0
- package/scripts/postinstall.js +202 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { exec, execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { resolveCredentialsDir } from "../config/paths.js";
|
|
7
|
+
export { resolveCredentialsDir };
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
export function resolveCredentialIndexPath() {
|
|
11
|
+
return path.join(resolveCredentialsDir(), "index.json");
|
|
12
|
+
}
|
|
13
|
+
export function resolveCredentialPath(service, account, authId) {
|
|
14
|
+
return path.join(resolveCredentialsDir(), service, "accounts", account, "auth", `${authId}.json`);
|
|
15
|
+
}
|
|
16
|
+
export function readCredentialRecordSync(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (!parsed || typeof parsed !== "object")
|
|
21
|
+
return null;
|
|
22
|
+
if (!parsed.type || !parsed.storage)
|
|
23
|
+
return null;
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function readCredentialRecord(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (!parsed || typeof parsed !== "object")
|
|
35
|
+
return null;
|
|
36
|
+
if (!parsed.type || !parsed.storage)
|
|
37
|
+
return null;
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function writeCredentialRecordSync(service, account, authId, record) {
|
|
45
|
+
const filePath = resolveCredentialPath(service, account, authId);
|
|
46
|
+
const dir = path.dirname(filePath);
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(filePath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
49
|
+
fs.chmodSync(filePath, 0o600);
|
|
50
|
+
}
|
|
51
|
+
export async function writeCredentialRecord(service, account, authId, record) {
|
|
52
|
+
const filePath = resolveCredentialPath(service, account, authId);
|
|
53
|
+
const dir = path.dirname(filePath);
|
|
54
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
55
|
+
await fsp.writeFile(filePath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
56
|
+
}
|
|
57
|
+
function isDirentDirectory(entry) {
|
|
58
|
+
return typeof entry !== "string";
|
|
59
|
+
}
|
|
60
|
+
export function listCredentialEntriesSync() {
|
|
61
|
+
const root = resolveCredentialsDir();
|
|
62
|
+
if (!fs.existsSync(root))
|
|
63
|
+
return [];
|
|
64
|
+
const services = fs.readdirSync(root, { withFileTypes: true });
|
|
65
|
+
const entries = [];
|
|
66
|
+
for (const serviceEntry of services) {
|
|
67
|
+
if (!isDirentDirectory(serviceEntry) || !serviceEntry.isDirectory())
|
|
68
|
+
continue;
|
|
69
|
+
const service = serviceEntry.name;
|
|
70
|
+
const accountsDir = path.join(root, service, "accounts");
|
|
71
|
+
if (!fs.existsSync(accountsDir))
|
|
72
|
+
continue;
|
|
73
|
+
const accounts = fs.readdirSync(accountsDir, { withFileTypes: true });
|
|
74
|
+
for (const accountEntry of accounts) {
|
|
75
|
+
if (!isDirentDirectory(accountEntry) || !accountEntry.isDirectory())
|
|
76
|
+
continue;
|
|
77
|
+
const account = accountEntry.name;
|
|
78
|
+
const authDir = path.join(accountsDir, account, "auth");
|
|
79
|
+
if (!fs.existsSync(authDir))
|
|
80
|
+
continue;
|
|
81
|
+
const authFiles = fs.readdirSync(authDir, { withFileTypes: true });
|
|
82
|
+
for (const authEntry of authFiles) {
|
|
83
|
+
if (!isDirentDirectory(authEntry) || authEntry.isDirectory())
|
|
84
|
+
continue;
|
|
85
|
+
if (!authEntry.name.endsWith(".json"))
|
|
86
|
+
continue;
|
|
87
|
+
const authId = authEntry.name.replace(/\.json$/, "");
|
|
88
|
+
const filePath = path.join(authDir, authEntry.name);
|
|
89
|
+
const record = readCredentialRecordSync(filePath);
|
|
90
|
+
if (!record)
|
|
91
|
+
continue;
|
|
92
|
+
entries.push({ service, account, authId, filePath, record });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return entries;
|
|
97
|
+
}
|
|
98
|
+
export async function listCredentialEntries() {
|
|
99
|
+
const root = resolveCredentialsDir();
|
|
100
|
+
if (!fs.existsSync(root))
|
|
101
|
+
return [];
|
|
102
|
+
const services = await fsp.readdir(root, { withFileTypes: true });
|
|
103
|
+
const entries = [];
|
|
104
|
+
for (const serviceEntry of services) {
|
|
105
|
+
if (!isDirentDirectory(serviceEntry) || !serviceEntry.isDirectory())
|
|
106
|
+
continue;
|
|
107
|
+
const service = serviceEntry.name;
|
|
108
|
+
const accountsDir = path.join(root, service, "accounts");
|
|
109
|
+
if (!fs.existsSync(accountsDir))
|
|
110
|
+
continue;
|
|
111
|
+
const accounts = await fsp.readdir(accountsDir, { withFileTypes: true });
|
|
112
|
+
for (const accountEntry of accounts) {
|
|
113
|
+
if (!isDirentDirectory(accountEntry) || !accountEntry.isDirectory())
|
|
114
|
+
continue;
|
|
115
|
+
const account = accountEntry.name;
|
|
116
|
+
const authDir = path.join(accountsDir, account, "auth");
|
|
117
|
+
if (!fs.existsSync(authDir))
|
|
118
|
+
continue;
|
|
119
|
+
const authFiles = await fsp.readdir(authDir, { withFileTypes: true });
|
|
120
|
+
for (const authEntry of authFiles) {
|
|
121
|
+
if (!isDirentDirectory(authEntry) || authEntry.isDirectory())
|
|
122
|
+
continue;
|
|
123
|
+
if (!authEntry.name.endsWith(".json"))
|
|
124
|
+
continue;
|
|
125
|
+
const authId = authEntry.name.replace(/\.json$/, "");
|
|
126
|
+
const filePath = path.join(authDir, authEntry.name);
|
|
127
|
+
const record = await readCredentialRecord(filePath);
|
|
128
|
+
if (!record)
|
|
129
|
+
continue;
|
|
130
|
+
entries.push({ service, account, authId, filePath, record });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return entries;
|
|
135
|
+
}
|
|
136
|
+
export function buildCredentialIndex(entries) {
|
|
137
|
+
const services = {};
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const service = services[entry.service] ?? { accounts: [] };
|
|
140
|
+
const account = service.accounts.find((acc) => acc.id === entry.account);
|
|
141
|
+
const status = entry.record.lastError
|
|
142
|
+
? "broken"
|
|
143
|
+
: entry.record.lastUsed
|
|
144
|
+
? "active"
|
|
145
|
+
: "ready";
|
|
146
|
+
if (account) {
|
|
147
|
+
if (!account.auths.includes(entry.authId))
|
|
148
|
+
account.auths.push(entry.authId);
|
|
149
|
+
if (entry.record.lastError) {
|
|
150
|
+
account.status = "broken";
|
|
151
|
+
account.lastError = entry.record.lastError;
|
|
152
|
+
}
|
|
153
|
+
else if (account.status !== "broken" && entry.record.lastUsed) {
|
|
154
|
+
account.status = "active";
|
|
155
|
+
account.lastUsed = entry.record.lastUsed;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
service.accounts.push({
|
|
160
|
+
id: entry.account,
|
|
161
|
+
owner: entry.record.owner,
|
|
162
|
+
auths: [entry.authId],
|
|
163
|
+
status,
|
|
164
|
+
lastUsed: entry.record.lastUsed,
|
|
165
|
+
lastError: entry.record.lastError ?? null,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
services[entry.service] = service;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
version: 1,
|
|
172
|
+
lastUpdated: new Date().toISOString(),
|
|
173
|
+
services,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function readCredentialIndexSync() {
|
|
177
|
+
const indexPath = resolveCredentialIndexPath();
|
|
178
|
+
try {
|
|
179
|
+
const raw = fs.readFileSync(indexPath, "utf-8");
|
|
180
|
+
const parsed = JSON.parse(raw);
|
|
181
|
+
if (!parsed || typeof parsed !== "object")
|
|
182
|
+
return null;
|
|
183
|
+
if (!parsed.services || typeof parsed.services !== "object")
|
|
184
|
+
return null;
|
|
185
|
+
return parsed;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export async function readCredentialIndex() {
|
|
192
|
+
const indexPath = resolveCredentialIndexPath();
|
|
193
|
+
try {
|
|
194
|
+
const raw = await fsp.readFile(indexPath, "utf-8");
|
|
195
|
+
const parsed = JSON.parse(raw);
|
|
196
|
+
if (!parsed || typeof parsed !== "object")
|
|
197
|
+
return null;
|
|
198
|
+
if (!parsed.services || typeof parsed.services !== "object")
|
|
199
|
+
return null;
|
|
200
|
+
return parsed;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export function writeCredentialIndexSync(index) {
|
|
207
|
+
const indexPath = resolveCredentialIndexPath();
|
|
208
|
+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
209
|
+
fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf-8");
|
|
210
|
+
}
|
|
211
|
+
export async function writeCredentialIndex(index) {
|
|
212
|
+
const indexPath = resolveCredentialIndexPath();
|
|
213
|
+
await fsp.mkdir(path.dirname(indexPath), { recursive: true });
|
|
214
|
+
await fsp.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf-8");
|
|
215
|
+
}
|
|
216
|
+
export function ensureCredentialIndexSync() {
|
|
217
|
+
const existing = readCredentialIndexSync();
|
|
218
|
+
if (existing)
|
|
219
|
+
return existing;
|
|
220
|
+
const index = buildCredentialIndex(listCredentialEntriesSync());
|
|
221
|
+
writeCredentialIndexSync(index);
|
|
222
|
+
return index;
|
|
223
|
+
}
|
|
224
|
+
function resolveStorageField(storage, field) {
|
|
225
|
+
if (storage.provider !== "1password")
|
|
226
|
+
return null;
|
|
227
|
+
const exact = storage.fields?.[field];
|
|
228
|
+
if (exact)
|
|
229
|
+
return exact;
|
|
230
|
+
if (field === "key")
|
|
231
|
+
return storage.fields?.apiKey ?? storage.fields?.token ?? null;
|
|
232
|
+
if (field === "accessToken")
|
|
233
|
+
return storage.fields?.accessToken ?? storage.fields?.token ?? null;
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
async function readSecretFromStorage(record, field) {
|
|
237
|
+
const storage = record.storage;
|
|
238
|
+
if (storage.provider === "plaintext") {
|
|
239
|
+
const value = field === "key" ? record.key : field === "token" ? record.token : record.accessToken;
|
|
240
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
241
|
+
}
|
|
242
|
+
if (storage.provider === "keychain") {
|
|
243
|
+
try {
|
|
244
|
+
const { stdout } = await execFileAsync("security", [
|
|
245
|
+
"find-generic-password",
|
|
246
|
+
"-s",
|
|
247
|
+
storage.service,
|
|
248
|
+
"-a",
|
|
249
|
+
storage.account,
|
|
250
|
+
"-w",
|
|
251
|
+
]);
|
|
252
|
+
const trimmed = stdout.trim();
|
|
253
|
+
return trimmed ? trimmed : null;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (storage.provider === "1password") {
|
|
260
|
+
const fieldName = resolveStorageField(storage, field);
|
|
261
|
+
if (!fieldName)
|
|
262
|
+
return null;
|
|
263
|
+
const itemRef = `op://${storage.vault}/${storage.item}/${fieldName}`;
|
|
264
|
+
try {
|
|
265
|
+
const { stdout } = await execFileAsync("op", ["read", itemRef]);
|
|
266
|
+
const trimmed = stdout.trim();
|
|
267
|
+
return trimmed ? trimmed : null;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (storage.provider === "external") {
|
|
274
|
+
try {
|
|
275
|
+
const { stdout } = await execAsync(storage.syncCommand);
|
|
276
|
+
const trimmed = stdout.trim();
|
|
277
|
+
return trimmed ? trimmed : null;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
export async function storeKeychainSecret(params) {
|
|
286
|
+
try {
|
|
287
|
+
await execFileAsync("security", [
|
|
288
|
+
"add-generic-password",
|
|
289
|
+
"-U",
|
|
290
|
+
"-s",
|
|
291
|
+
params.service,
|
|
292
|
+
"-a",
|
|
293
|
+
params.account,
|
|
294
|
+
"-w",
|
|
295
|
+
params.value,
|
|
296
|
+
]);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export async function resolveCredentialValue(record) {
|
|
304
|
+
if (record.type === "api_key") {
|
|
305
|
+
const value = await readSecretFromStorage(record, "key");
|
|
306
|
+
if (!value)
|
|
307
|
+
return null;
|
|
308
|
+
return { value, field: "key" };
|
|
309
|
+
}
|
|
310
|
+
if (record.type === "token") {
|
|
311
|
+
const value = await readSecretFromStorage(record, "token");
|
|
312
|
+
if (!value)
|
|
313
|
+
return null;
|
|
314
|
+
return { value, field: "token" };
|
|
315
|
+
}
|
|
316
|
+
if (record.type === "oauth") {
|
|
317
|
+
const value = await readSecretFromStorage(record, "accessToken");
|
|
318
|
+
if (!value)
|
|
319
|
+
return null;
|
|
320
|
+
return { value, field: "accessToken" };
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { loadConfig } from "../config/config.js";
|
|
2
|
+
const DEFAULT_REDACT_MODE = "tools";
|
|
3
|
+
const DEFAULT_REDACT_MIN_LENGTH = 18;
|
|
4
|
+
const DEFAULT_REDACT_KEEP_START = 6;
|
|
5
|
+
const DEFAULT_REDACT_KEEP_END = 4;
|
|
6
|
+
const DEFAULT_REDACT_PATTERNS = [
|
|
7
|
+
// ENV-style assignments.
|
|
8
|
+
String.raw `\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)\b\s*[=:]\s*(["']?)([^\s"'\\]+)\1`,
|
|
9
|
+
// JSON fields.
|
|
10
|
+
String.raw `"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"`,
|
|
11
|
+
// CLI flags.
|
|
12
|
+
String.raw `--(?:api[-_]?key|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1`,
|
|
13
|
+
// Authorization headers.
|
|
14
|
+
String.raw `Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)`,
|
|
15
|
+
String.raw `\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b`,
|
|
16
|
+
// PEM blocks.
|
|
17
|
+
String.raw `-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----`,
|
|
18
|
+
// Common token prefixes.
|
|
19
|
+
String.raw `\b(sk-[A-Za-z0-9_-]{8,})\b`,
|
|
20
|
+
String.raw `\b(ghp_[A-Za-z0-9]{20,})\b`,
|
|
21
|
+
String.raw `\b(github_pat_[A-Za-z0-9_]{20,})\b`,
|
|
22
|
+
String.raw `\b(xox[baprs]-[A-Za-z0-9-]{10,})\b`,
|
|
23
|
+
String.raw `\b(xapp-[A-Za-z0-9-]{10,})\b`,
|
|
24
|
+
String.raw `\b(gsk_[A-Za-z0-9_-]{10,})\b`,
|
|
25
|
+
String.raw `\b(AIza[0-9A-Za-z\-_]{20,})\b`,
|
|
26
|
+
String.raw `\b(pplx-[A-Za-z0-9_-]{10,})\b`,
|
|
27
|
+
String.raw `\b(npm_[A-Za-z0-9]{10,})\b`,
|
|
28
|
+
String.raw `\b(\d{6,}:[A-Za-z0-9_-]{20,})\b`,
|
|
29
|
+
];
|
|
30
|
+
function normalizeMode(value) {
|
|
31
|
+
return value === "off" ? "off" : DEFAULT_REDACT_MODE;
|
|
32
|
+
}
|
|
33
|
+
function parsePattern(raw) {
|
|
34
|
+
if (!raw.trim())
|
|
35
|
+
return null;
|
|
36
|
+
const match = raw.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
37
|
+
try {
|
|
38
|
+
if (match) {
|
|
39
|
+
const flags = match[2].includes("g") ? match[2] : `${match[2]}g`;
|
|
40
|
+
return new RegExp(match[1], flags);
|
|
41
|
+
}
|
|
42
|
+
return new RegExp(raw, "gi");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function resolvePatterns(value) {
|
|
49
|
+
const source = value?.length ? value : DEFAULT_REDACT_PATTERNS;
|
|
50
|
+
return source.map(parsePattern).filter((re) => Boolean(re));
|
|
51
|
+
}
|
|
52
|
+
function maskToken(token) {
|
|
53
|
+
if (token.length < DEFAULT_REDACT_MIN_LENGTH)
|
|
54
|
+
return "***";
|
|
55
|
+
const start = token.slice(0, DEFAULT_REDACT_KEEP_START);
|
|
56
|
+
const end = token.slice(-DEFAULT_REDACT_KEEP_END);
|
|
57
|
+
return `${start}…${end}`;
|
|
58
|
+
}
|
|
59
|
+
function redactPemBlock(block) {
|
|
60
|
+
const lines = block.split(/\r?\n/).filter(Boolean);
|
|
61
|
+
if (lines.length < 2)
|
|
62
|
+
return "***";
|
|
63
|
+
return `${lines[0]}\n…redacted…\n${lines[lines.length - 1]}`;
|
|
64
|
+
}
|
|
65
|
+
function redactMatch(match, groups) {
|
|
66
|
+
if (match.includes("PRIVATE KEY-----"))
|
|
67
|
+
return redactPemBlock(match);
|
|
68
|
+
const token = groups
|
|
69
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
70
|
+
.at(-1) ?? match;
|
|
71
|
+
const masked = maskToken(token);
|
|
72
|
+
if (token === match)
|
|
73
|
+
return masked;
|
|
74
|
+
return match.replace(token, masked);
|
|
75
|
+
}
|
|
76
|
+
function redactText(text, patterns) {
|
|
77
|
+
let next = text;
|
|
78
|
+
for (const pattern of patterns) {
|
|
79
|
+
next = next.replace(pattern, (...args) => redactMatch(args[0], args.slice(1, args.length - 2)));
|
|
80
|
+
}
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
function resolveConfigRedaction() {
|
|
84
|
+
const cfg = loadConfig().logging;
|
|
85
|
+
return {
|
|
86
|
+
mode: normalizeMode(cfg?.redactSensitive),
|
|
87
|
+
patterns: cfg?.redactPatterns,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function redactSensitiveText(text, options) {
|
|
91
|
+
if (!text)
|
|
92
|
+
return text;
|
|
93
|
+
const resolved = options ?? resolveConfigRedaction();
|
|
94
|
+
if (normalizeMode(resolved.mode) === "off")
|
|
95
|
+
return text;
|
|
96
|
+
const patterns = resolvePatterns(resolved.patterns);
|
|
97
|
+
if (!patterns.length)
|
|
98
|
+
return text;
|
|
99
|
+
return redactText(text, patterns);
|
|
100
|
+
}
|
|
101
|
+
export function redactToolDetail(detail) {
|
|
102
|
+
const resolved = resolveConfigRedaction();
|
|
103
|
+
if (normalizeMode(resolved.mode) !== "tools")
|
|
104
|
+
return detail;
|
|
105
|
+
return redactSensitiveText(detail, resolved);
|
|
106
|
+
}
|
|
107
|
+
export function getDefaultRedactPatterns() {
|
|
108
|
+
return [...DEFAULT_REDACT_PATTERNS];
|
|
109
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function parseFenceSpans(buffer) {
|
|
2
|
+
const spans = [];
|
|
3
|
+
let open;
|
|
4
|
+
let offset = 0;
|
|
5
|
+
while (offset <= buffer.length) {
|
|
6
|
+
const nextNewline = buffer.indexOf("\n", offset);
|
|
7
|
+
const lineEnd = nextNewline === -1 ? buffer.length : nextNewline;
|
|
8
|
+
const line = buffer.slice(offset, lineEnd);
|
|
9
|
+
const match = line.match(/^( {0,3})(`{3,}|~{3,})(.*)$/);
|
|
10
|
+
if (match) {
|
|
11
|
+
const indent = match[1];
|
|
12
|
+
const marker = match[2];
|
|
13
|
+
const markerChar = marker[0];
|
|
14
|
+
const markerLen = marker.length;
|
|
15
|
+
if (!open) {
|
|
16
|
+
open = {
|
|
17
|
+
start: offset,
|
|
18
|
+
markerChar,
|
|
19
|
+
markerLen,
|
|
20
|
+
openLine: line,
|
|
21
|
+
marker,
|
|
22
|
+
indent,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
else if (open.markerChar === markerChar &&
|
|
26
|
+
markerLen >= open.markerLen) {
|
|
27
|
+
const end = nextNewline === -1 ? buffer.length : nextNewline + 1;
|
|
28
|
+
spans.push({
|
|
29
|
+
start: open.start,
|
|
30
|
+
end,
|
|
31
|
+
openLine: open.openLine,
|
|
32
|
+
marker: open.marker,
|
|
33
|
+
indent: open.indent,
|
|
34
|
+
});
|
|
35
|
+
open = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (nextNewline === -1)
|
|
39
|
+
break;
|
|
40
|
+
offset = nextNewline + 1;
|
|
41
|
+
}
|
|
42
|
+
if (open) {
|
|
43
|
+
spans.push({
|
|
44
|
+
start: open.start,
|
|
45
|
+
end: buffer.length,
|
|
46
|
+
openLine: open.openLine,
|
|
47
|
+
marker: open.marker,
|
|
48
|
+
indent: open.indent,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return spans;
|
|
52
|
+
}
|
|
53
|
+
export function findFenceSpanAt(spans, index) {
|
|
54
|
+
return spans.find((span) => index > span.start && index < span.end);
|
|
55
|
+
}
|
|
56
|
+
export function isSafeFenceBreak(spans, index) {
|
|
57
|
+
return !findFenceSpanAt(spans, index);
|
|
58
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { resolveApiKeyForProvider } from "../agents/model-auth.js";
|
|
2
|
+
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
3
|
+
const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
|
|
4
|
+
function normalizeOpenAiModel(model) {
|
|
5
|
+
const trimmed = model.trim();
|
|
6
|
+
if (!trimmed)
|
|
7
|
+
return "text-embedding-3-small";
|
|
8
|
+
if (trimmed.startsWith("openai/"))
|
|
9
|
+
return trimmed.slice("openai/".length);
|
|
10
|
+
return trimmed;
|
|
11
|
+
}
|
|
12
|
+
async function createOpenAiEmbeddingProvider(options) {
|
|
13
|
+
const remote = options.remote;
|
|
14
|
+
const remoteApiKey = remote?.apiKey?.trim();
|
|
15
|
+
const remoteBaseUrl = remote?.baseUrl?.trim();
|
|
16
|
+
const { apiKey } = remoteApiKey
|
|
17
|
+
? { apiKey: remoteApiKey }
|
|
18
|
+
: await resolveApiKeyForProvider({
|
|
19
|
+
provider: "openai",
|
|
20
|
+
cfg: options.config,
|
|
21
|
+
agentDir: options.agentDir,
|
|
22
|
+
});
|
|
23
|
+
const providerConfig = options.config.models?.providers?.openai;
|
|
24
|
+
const baseUrl = remoteBaseUrl || providerConfig?.baseUrl?.trim() || DEFAULT_OPENAI_BASE_URL;
|
|
25
|
+
const url = `${baseUrl.replace(/\/$/, "")}/embeddings`;
|
|
26
|
+
const headerOverrides = Object.assign({}, providerConfig?.headers, remote?.headers);
|
|
27
|
+
const headers = {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
Authorization: `Bearer ${apiKey}`,
|
|
30
|
+
...headerOverrides,
|
|
31
|
+
};
|
|
32
|
+
const model = normalizeOpenAiModel(options.model);
|
|
33
|
+
const embed = async (input) => {
|
|
34
|
+
if (input.length === 0)
|
|
35
|
+
return [];
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers,
|
|
39
|
+
body: JSON.stringify({ model, input }),
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const text = await res.text();
|
|
43
|
+
throw new Error(`openai embeddings failed: ${res.status} ${text}`);
|
|
44
|
+
}
|
|
45
|
+
const payload = (await res.json());
|
|
46
|
+
const data = payload.data ?? [];
|
|
47
|
+
return data.map((entry) => entry.embedding ?? []);
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
id: "openai",
|
|
51
|
+
model,
|
|
52
|
+
embedQuery: async (text) => {
|
|
53
|
+
const [vec] = await embed([text]);
|
|
54
|
+
return vec ?? [];
|
|
55
|
+
},
|
|
56
|
+
embedBatch: embed,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function createLocalEmbeddingProvider(options) {
|
|
60
|
+
const modelPath = options.local?.modelPath?.trim() || DEFAULT_LOCAL_MODEL;
|
|
61
|
+
const modelCacheDir = options.local?.modelCacheDir?.trim();
|
|
62
|
+
// Lazy-load node-llama-cpp to keep startup light unless local is enabled.
|
|
63
|
+
const moduleName = "node-llama-cpp";
|
|
64
|
+
const { getLlama, resolveModelFile, LlamaLogLevel } = (await import(moduleName));
|
|
65
|
+
let llama = null;
|
|
66
|
+
let embeddingModel = null;
|
|
67
|
+
let embeddingContext = null;
|
|
68
|
+
const ensureContext = async () => {
|
|
69
|
+
if (!llama) {
|
|
70
|
+
llama = await getLlama({ logLevel: LlamaLogLevel.error });
|
|
71
|
+
}
|
|
72
|
+
if (!embeddingModel) {
|
|
73
|
+
const resolved = await resolveModelFile(modelPath, modelCacheDir || undefined);
|
|
74
|
+
embeddingModel = await llama.loadModel({ modelPath: resolved });
|
|
75
|
+
}
|
|
76
|
+
if (!embeddingContext) {
|
|
77
|
+
embeddingContext = await embeddingModel.createEmbeddingContext();
|
|
78
|
+
}
|
|
79
|
+
return embeddingContext;
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
id: "local",
|
|
83
|
+
model: modelPath,
|
|
84
|
+
embedQuery: async (text) => {
|
|
85
|
+
const ctx = await ensureContext();
|
|
86
|
+
const embedding = await ctx.getEmbeddingFor(text);
|
|
87
|
+
return Array.from(embedding.vector);
|
|
88
|
+
},
|
|
89
|
+
embedBatch: async (texts) => {
|
|
90
|
+
const ctx = await ensureContext();
|
|
91
|
+
const embeddings = await Promise.all(texts.map(async (text) => {
|
|
92
|
+
const embedding = await ctx.getEmbeddingFor(text);
|
|
93
|
+
return Array.from(embedding.vector);
|
|
94
|
+
}));
|
|
95
|
+
return embeddings;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export async function createEmbeddingProvider(options) {
|
|
100
|
+
const requestedProvider = options.provider;
|
|
101
|
+
if (options.provider === "local") {
|
|
102
|
+
try {
|
|
103
|
+
const provider = await createLocalEmbeddingProvider(options);
|
|
104
|
+
return { provider, requestedProvider };
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const reason = formatLocalSetupError(err);
|
|
108
|
+
if (options.fallback === "openai") {
|
|
109
|
+
try {
|
|
110
|
+
const provider = await createOpenAiEmbeddingProvider(options);
|
|
111
|
+
return {
|
|
112
|
+
provider,
|
|
113
|
+
requestedProvider,
|
|
114
|
+
fallbackFrom: "local",
|
|
115
|
+
fallbackReason: reason,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (fallbackErr) {
|
|
119
|
+
throw new Error(`${reason}\n\nFallback to OpenAI failed: ${formatError(fallbackErr)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw new Error(reason);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const provider = await createOpenAiEmbeddingProvider(options);
|
|
126
|
+
return { provider, requestedProvider };
|
|
127
|
+
}
|
|
128
|
+
function formatError(err) {
|
|
129
|
+
if (err instanceof Error)
|
|
130
|
+
return err.message;
|
|
131
|
+
return String(err);
|
|
132
|
+
}
|
|
133
|
+
function formatLocalSetupError(err) {
|
|
134
|
+
const detail = formatError(err);
|
|
135
|
+
return [
|
|
136
|
+
"Local embeddings unavailable.",
|
|
137
|
+
detail ? `Reason: ${detail}` : undefined,
|
|
138
|
+
"To enable local embeddings:",
|
|
139
|
+
"1) pnpm approve-builds",
|
|
140
|
+
"2) select node-llama-cpp",
|
|
141
|
+
"3) pnpm rebuild node-llama-cpp",
|
|
142
|
+
'Or set agent.memorySearch.provider = "openai" (remote).',
|
|
143
|
+
]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join("\n");
|
|
146
|
+
}
|