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 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.html) · [Quality & trust](https://sfi.auditforce.cloud/trust.html)
122
- - [Capabilities](https://sfi.auditforce.cloud/capabilities.html) · [All tools](https://sfi.auditforce.cloud/tools.html)
123
- - [Configuration](https://sfi.auditforce.cloud/configuration.html) · [FAQ](https://sfi.auditforce.cloud/faq.html)
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.html>.
113
+ <https://sfi.auditforce.cloud/licensing>.
package/bin/sfi.js CHANGED
@@ -1,5 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { createProgram } from '../dist/index.js';
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)
@@ -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 soqlFrom = (queryCtx) => {
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
- reads.add(`${obj}.${fn.getText()}`);
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 });