sf-intelligence 0.3.3 → 0.4.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 +7 -22
- package/bin/sfi.js +13 -1
- package/dist/apex-ast-worker.js +49 -6
- package/dist/index.js +23596 -14753
- package/package.json +2 -2
- package/server.json +5 -5
package/README.md
CHANGED
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
# sf-intelligence
|
|
2
2
|
|
|
3
|
+
Ask your AI what breaks before you change Salesforce. Offline, read-only, cited answers. Website and docs: **[sfi.auditforce.cloud](https://sfi.auditforce.cloud)**
|
|
4
|
+
|
|
3
5
|
A **grounded, fail-closed backend for AI assistants** working in one Salesforce org — answers come from the org's **real metadata**, not a guess.
|
|
4
6
|
|
|
5
7
|
`sf-intelligence` is an **offline-first, read-only, MCP-first knowledge base** for a single Salesforce org. One `sf project retrieve` builds a local vault (Markdown + a DuckDB dependency graph); a semantic router **advises** a ranked tool shortlist and your **host LLM decides** which to run. It **fails closed** — write imperatives and prompt injection are refused by shape, an unanswerable ask gets an honest gap instead of a lookalike tool, and genuine ambiguity gets a clarifying question instead of a guess. MIT + Commons Clause.
|
|
6
8
|
|
|
7
|
-
Requires **Node.js 20+**. `npx -y sf-intelligence …` needs no install; `npm install -g sf-intelligence` puts `sfi` on your PATH for shorter commands.
|
|
8
|
-
|
|
9
|
-
## Upgrading to 0.3.0 (breaking)
|
|
10
|
-
|
|
11
|
-
Coming from 0.2.x? Read this first — full detail in
|
|
12
|
-
[CHANGELOG.md](https://github.com/PranavNagrecha/Salesforce-Intelligence/blob/main/CHANGELOG.md).
|
|
13
|
-
|
|
14
|
-
- **`SFI_TOOL_PROFILE` now defaults to `core`.** 25 tools are advertised and
|
|
15
|
-
directly invokable; the rest are reached via
|
|
16
|
-
`sfi.run_analysis { name: 'sfi.<tool>', args }`. `SFI_TOOL_PROFILE=full`
|
|
17
|
-
restores advertise-and-invoke-everything.
|
|
18
|
-
- **`liveEnabled: true` no longer opens the live plane.** Grant standing
|
|
19
|
-
consent with `sfi.live_consent { grant: true }` or set
|
|
20
|
-
`SFI_LIVE_PLANE_ENABLED=1` — existing on-disk live grants stop working, so
|
|
21
|
-
re-grant once.
|
|
22
|
-
- **The update check is now opt-in** (`SFI_UPDATE_CHECK=1`), and every
|
|
23
|
-
success envelope gains a `contentPolicy` block marking org metadata as
|
|
24
|
-
untrusted data for hosts.
|
|
9
|
+
Requires **Node.js 20+**. `npx -y sf-intelligence …` needs no install; `npm install -g sf-intelligence` puts `sfi` on your PATH for shorter commands. Upgrading from an earlier version? Breaking changes and upgrade steps for every release are in the [CHANGELOG](https://github.com/PranavNagrecha/Salesforce-Intelligence/blob/main/CHANGELOG.md).
|
|
25
10
|
|
|
26
11
|
## Try it now — no Salesforce org needed
|
|
27
12
|
|
|
@@ -118,11 +103,11 @@ Share it, or just describe the gap, at <https://github.com/PranavNagrecha/Salesf
|
|
|
118
103
|
|
|
119
104
|
Full guides, capabilities, the tool catalog, and configuration: **https://sfi.auditforce.cloud**
|
|
120
105
|
|
|
121
|
-
- [Getting started](https://sfi.auditforce.cloud/getting-started
|
|
122
|
-
- [Capabilities](https://sfi.auditforce.cloud/capabilities
|
|
123
|
-
- [Configuration](https://sfi.auditforce.cloud/configuration
|
|
106
|
+
- [Try the demo](https://sfi.auditforce.cloud/demo) · [Getting started](https://sfi.auditforce.cloud/getting-started) · [Quality & trust](https://sfi.auditforce.cloud/trust)
|
|
107
|
+
- [Capabilities](https://sfi.auditforce.cloud/capabilities) · [All tools](https://sfi.auditforce.cloud/tools)
|
|
108
|
+
- [Configuration](https://sfi.auditforce.cloud/configuration) · [FAQ](https://sfi.auditforce.cloud/faq)
|
|
124
109
|
|
|
125
110
|
## License
|
|
126
111
|
|
|
127
112
|
MIT + Commons Clause — see the `LICENSE` file shipped in this package, or
|
|
128
|
-
<https://sfi.auditforce.cloud/licensing
|
|
113
|
+
<https://sfi.auditforce.cloud/licensing>.
|
package/bin/sfi.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// PERF-11: the CLI is one ~8 MB bundle that every invocation (including the
|
|
3
|
+
// MCP server a host spawns) parses from scratch. Node >= 22.1 can cache the
|
|
4
|
+
// compiled code on disk; enable it before loading the bundle. Best-effort:
|
|
5
|
+
// older Node lacks the API, and NODE_DISABLE_COMPILE_CACHE=1 turns it off.
|
|
6
|
+
import module from 'node:module';
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
module.enableCompileCache?.();
|
|
10
|
+
} catch {
|
|
11
|
+
// A cache that cannot be created only costs speed.
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { createProgram } = await import('../dist/index.js');
|
|
3
15
|
|
|
4
16
|
createProgram()
|
|
5
17
|
.parseAsync(process.argv)
|
package/dist/apex-ast-worker.js
CHANGED
|
@@ -44,6 +44,21 @@ var SYSTEM_TYPES = /* @__PURE__ */ new Set([
|
|
|
44
44
|
"Http",
|
|
45
45
|
"Trigger"
|
|
46
46
|
]);
|
|
47
|
+
var elementTypeOf = (t) => {
|
|
48
|
+
if (t === null)
|
|
49
|
+
return null;
|
|
50
|
+
const compact = t.replace(/\s+/g, "");
|
|
51
|
+
const arr = /^(.+)\[\]$/.exec(compact);
|
|
52
|
+
if (arr !== null)
|
|
53
|
+
return arr[1] ?? null;
|
|
54
|
+
const list = /^(?:List|Set)<(.+)>$/i.exec(compact);
|
|
55
|
+
if (list !== null)
|
|
56
|
+
return list[1] ?? null;
|
|
57
|
+
const map = /^Map<[^,<>]+,(.+)>$/i.exec(compact);
|
|
58
|
+
if (map !== null)
|
|
59
|
+
return map[1] ?? null;
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
47
62
|
var SYSTEM_CALL_ALLOW = {
|
|
48
63
|
Database: "*",
|
|
49
64
|
Type: /* @__PURE__ */ new Set(["forName"]),
|
|
@@ -79,6 +94,9 @@ var Collecting = class extends ApexErrorListener {
|
|
|
79
94
|
};
|
|
80
95
|
var extractApexAstEdges = (source, className, options = {}) => {
|
|
81
96
|
const known = options.knownClasses ?? /* @__PURE__ */ new Set();
|
|
97
|
+
const objectCase = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const o of options.knownObjects ?? [])
|
|
99
|
+
objectCase.set(o.toLowerCase(), o);
|
|
82
100
|
const listener = new Collecting();
|
|
83
101
|
let tree;
|
|
84
102
|
try {
|
|
@@ -114,6 +132,7 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
114
132
|
if (idx === 0 && ext >= 0)
|
|
115
133
|
extendsType = kidList[ext + 1]?.getText() ?? null;
|
|
116
134
|
});
|
|
135
|
+
const canonicalType = (t) => objectCase.size === 0 ? t : t.replace(/[A-Za-z_][A-Za-z0-9_]*/g, (tok) => objectCase.get(tok.toLowerCase()) ?? tok);
|
|
117
136
|
const scopeFor = (node) => {
|
|
118
137
|
const md = ancestorWhere(node, (c) => ctxName(c) === "MethodDeclaration");
|
|
119
138
|
if (md === null)
|
|
@@ -128,7 +147,7 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
128
147
|
const declareVar = (typeText, varName, scope) => {
|
|
129
148
|
if (typeText === void 0 || varName === void 0 || typeText.length === 0)
|
|
130
149
|
return;
|
|
131
|
-
scope.set(varName.toLowerCase(), typeText.trim());
|
|
150
|
+
scope.set(varName.toLowerCase(), canonicalType(typeText.trim()));
|
|
132
151
|
};
|
|
133
152
|
for (const md of findAll(tree, "MethodDeclaration")) {
|
|
134
153
|
const id = kids(md).find((k) => ctxName(k) === "Id")?.getText();
|
|
@@ -137,7 +156,7 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
137
156
|
ownMethods.add(id.toLowerCase());
|
|
138
157
|
const ret = kids(md).find((k) => ctxName(k) === "TypeRef")?.getText();
|
|
139
158
|
if (ret !== void 0 && ret.length > 0) {
|
|
140
|
-
methodReturnTypes.set(id.toLowerCase(), ret.trim());
|
|
159
|
+
methodReturnTypes.set(id.toLowerCase(), canonicalType(ret.trim()));
|
|
141
160
|
}
|
|
142
161
|
}
|
|
143
162
|
for (const fd of findAll(tree, "FieldDeclaration")) {
|
|
@@ -210,7 +229,8 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
210
229
|
const txt = findAll(list, "FieldName")[0]?.getText();
|
|
211
230
|
return txt === void 0 || txt.length === 0 ? void 0 : txt;
|
|
212
231
|
};
|
|
213
|
-
const
|
|
232
|
+
const literalReads = /* @__PURE__ */ new Set();
|
|
233
|
+
const soqlFrom = (queryCtx, into = reads) => {
|
|
214
234
|
const SCOPE = /* @__PURE__ */ new Set(["Query", "SubQuery"]);
|
|
215
235
|
for (const fn of findAll(queryCtx, "FieldName")) {
|
|
216
236
|
if (ancestorWhere(fn, (c) => ctxName(c) === "FromNameList", SCOPE) !== null)
|
|
@@ -226,7 +246,7 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
226
246
|
const obj = FROM_OBJ(scope);
|
|
227
247
|
if (obj === void 0)
|
|
228
248
|
continue;
|
|
229
|
-
|
|
249
|
+
into.add(`${obj}.${fn.getText()}`);
|
|
230
250
|
}
|
|
231
251
|
};
|
|
232
252
|
for (const q of findAll(tree, "Query"))
|
|
@@ -241,7 +261,7 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
241
261
|
qp.addErrorListener(c);
|
|
242
262
|
const qt = qp.query();
|
|
243
263
|
if (c.errors.length === 0)
|
|
244
|
-
soqlFrom(qt);
|
|
264
|
+
soqlFrom(qt, literalReads);
|
|
245
265
|
} catch {
|
|
246
266
|
}
|
|
247
267
|
}
|
|
@@ -267,7 +287,13 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
267
287
|
const resolveRootType = (seg) => {
|
|
268
288
|
const rootLower = seg.root.toLowerCase();
|
|
269
289
|
if (/^new\s*/i.test(seg.root) || ctxName(seg.rootNode) === "NewExpression") {
|
|
270
|
-
return seg.root.replace(/^new\s*/i, "").replace(/\(.*\)$/, "").replace(/[<>].*$/, "");
|
|
290
|
+
return canonicalType(seg.root.replace(/^new\s*/i, "").replace(/\(.*\)$/, "").replace(/[<>].*$/, ""));
|
|
291
|
+
}
|
|
292
|
+
if (ctxName(seg.rootNode) === "ArrayExpression") {
|
|
293
|
+
const base = kids(seg.rootNode)[0];
|
|
294
|
+
if (base === void 0 || ctxName(base) !== "PrimaryExpression")
|
|
295
|
+
return null;
|
|
296
|
+
return elementTypeOf(resolveVarType(base.getText(), base));
|
|
271
297
|
}
|
|
272
298
|
if (rootLower === "this")
|
|
273
299
|
return className;
|
|
@@ -303,6 +329,8 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
303
329
|
if (record)
|
|
304
330
|
recordCall(`${type}.${method}`, at);
|
|
305
331
|
type = null;
|
|
332
|
+
} else if (method.toLowerCase() === "get" && elementTypeOf(type) !== null) {
|
|
333
|
+
type = elementTypeOf(type);
|
|
306
334
|
} else {
|
|
307
335
|
type = null;
|
|
308
336
|
}
|
|
@@ -384,11 +412,22 @@ var extractApexAstEdges = (source, className, options = {}) => {
|
|
|
384
412
|
if (id !== void 0 && ownMethods.has(id.toLowerCase()))
|
|
385
413
|
recordCall(`${className}.${id}`, mc);
|
|
386
414
|
}
|
|
415
|
+
const literalOnly = [...literalReads].filter((r) => !reads.has(r));
|
|
416
|
+
for (const r of literalOnly)
|
|
417
|
+
reads.add(r);
|
|
387
418
|
return {
|
|
388
419
|
calls: [...calls].sort(),
|
|
389
420
|
reads: [...reads].sort(),
|
|
421
|
+
...literalOnly.length > 0 ? { literalQueryReads: literalOnly.sort() } : {},
|
|
390
422
|
writes: [...writes].sort(),
|
|
391
423
|
innerTypes: [...innerClasses.keys()].sort(),
|
|
424
|
+
variables: [
|
|
425
|
+
.../* @__PURE__ */ new Set([
|
|
426
|
+
...fieldTypes.keys(),
|
|
427
|
+
...orphanLocals.keys(),
|
|
428
|
+
...[...methodLocals.values()].flatMap((m) => [...m.keys()])
|
|
429
|
+
])
|
|
430
|
+
].sort(),
|
|
392
431
|
callSites: [...callSites].sort((a, b) => a.callee < b.callee ? -1 : a.callee > b.callee ? 1 : a.callerMethod < b.callerMethod ? -1 : a.callerMethod > b.callerMethod ? 1 : 0)
|
|
393
432
|
};
|
|
394
433
|
};
|
|
@@ -400,9 +439,13 @@ if (parentPort === null) {
|
|
|
400
439
|
var knownClasses = new Set(
|
|
401
440
|
Array.isArray(workerData?.knownClasses) ? workerData.knownClasses : []
|
|
402
441
|
);
|
|
442
|
+
var knownObjects = new Set(
|
|
443
|
+
Array.isArray(workerData?.knownObjects) ? workerData.knownObjects : []
|
|
444
|
+
);
|
|
403
445
|
parentPort.on("message", (job) => {
|
|
404
446
|
const result = extractApexAstEdges(job.source, job.apiName, {
|
|
405
447
|
knownClasses,
|
|
448
|
+
knownObjects,
|
|
406
449
|
kind: job.kind
|
|
407
450
|
});
|
|
408
451
|
parentPort.postMessage({ index: job.index, result });
|