getprismo 0.1.59 → 0.1.61

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/docs/manual.md CHANGED
@@ -1055,6 +1055,15 @@ npx getprismo context backend
1055
1055
 
1056
1056
  use these as the starting point for coding sessions instead of letting agents explore the whole repo.
1057
1057
 
1058
+ Each generated context pack also gets a machine-readable receipt next to it, for example:
1059
+
1060
+ ```text
1061
+ .prismo/frontend-context.md
1062
+ .prismo/frontend-context.receipt.json
1063
+ ```
1064
+
1065
+ Receipts record the repo ref, intended agent surfaces, source roots, omitted context classes, a digest of the source-file slice, estimated pack tokens, stale-after policy, and the command to regenerate/verify the pack. They do not include raw prompts or source contents; the digest is an audit handle so later sessions and the dashboard can tell what a pack represents without treating it as opaque markdown.
1066
+
1058
1067
  ---
1059
1068
 
1060
1069
  ## tracking modes
@@ -1081,8 +1090,13 @@ no api keys. no intercepted prompts. no data uploaded.
1081
1090
  ├── architecture-summary.md
1082
1091
  ├── backend-summary.md
1083
1092
  ├── frontend-summary.md
1093
+ ├── architecture-summary.receipt.json
1094
+ ├── backend-summary.receipt.json
1095
+ ├── frontend-summary.receipt.json
1084
1096
  ├── frontend-context.md
1097
+ ├── frontend-context.receipt.json
1085
1098
  ├── backend-context.md
1099
+ ├── backend-context.receipt.json
1086
1100
  ├── recommended-CLAUDE.boilerplate.md
1087
1101
  ├── recommended-AGENTS.boilerplate.md
1088
1102
  ├── recommended-.claudeignore
@@ -386,7 +386,9 @@ module.exports = function createCloudSync(deps) {
386
386
  }
387
387
 
388
388
  async function runSync(rootDir = process.cwd(), options = {}) {
389
- const config = loadConfig();
389
+ // Allow an embedding host (the editor extension) to inject its own
390
+ // connection instead of reading the CLI's on-disk config.
391
+ const config = options.config || loadConfig();
390
392
  const payload = buildSyncPayload(rootDir, {
391
393
  limit: options.limit || config?.sync?.defaultLimit || 20,
392
394
  tool: options.tool || "all",
@@ -1,14 +1,17 @@
1
1
  module.exports = function createContextOptimize(deps) {
2
+ const crypto = require("crypto");
3
+ const { spawnSync } = require("child_process");
2
4
  const {
3
5
  fs,
4
6
  path,
5
7
  NPX_COMMAND,
8
+ PACKAGE_VERSION,
9
+ estimateTokens,
6
10
  scanRepo,
7
11
  safeReadJson,
8
12
  readIfText,
9
13
  formatBytes,
10
14
  color,
11
- writeGeneratedFile,
12
15
  } = deps;
13
16
 
14
17
  const {
@@ -125,6 +128,145 @@ function proseList(items, empty = "none detected") {
125
128
  return items && items.length ? items.join(", ") : empty;
126
129
  }
127
130
 
131
+ function sha256(value) {
132
+ return `sha256:${crypto.createHash("sha256").update(String(value)).digest("hex")}`;
133
+ }
134
+
135
+ function gitValue(root, args) {
136
+ try {
137
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
138
+ return result.status === 0 ? result.stdout.trim() || null : null;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ function workingTreeDirty(root) {
145
+ try {
146
+ const result = spawnSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
147
+ if (result.status !== 0) return null;
148
+ return result.stdout.trim().length > 0;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ function repoRef(root) {
155
+ const branch = gitValue(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
156
+ const commit = gitValue(root, ["rev-parse", "--short=12", "HEAD"]);
157
+ // A pack derived from uncommitted files has a different staleness boundary
158
+ // than one derived from a clean commit, so record the working-tree state.
159
+ // null when this is not a git repo.
160
+ const inRepo = Boolean(branch || commit);
161
+ return {
162
+ branch,
163
+ commit,
164
+ working_tree_dirty: inRepo ? workingTreeDirty(root) : null,
165
+ };
166
+ }
167
+
168
+ // Directory roots that are intentionally kept out of context grounding (they
169
+ // map to the receipt's omitted_classes). Used to separate roots that actually
170
+ // grounded the pack from roots that were merely present and avoided.
171
+ const EXCLUDED_ROOT_DIRS = new Set([
172
+ "node_modules", "vendor", ".venv", "venv",
173
+ "dist", "build", "out", "target", ".next", ".nuxt", ".output", ".svelte-kit",
174
+ "coverage", ".nyc_output",
175
+ "logs", "log",
176
+ ".cache", "__pycache__", ".git",
177
+ ]);
178
+
179
+ function isExcludedRoot(root) {
180
+ return String(root || "").split("/").some((seg) => EXCLUDED_ROOT_DIRS.has(seg.toLowerCase()));
181
+ }
182
+
183
+ function isContextPackFile(name) {
184
+ return name === "architecture-summary.md" ||
185
+ name === "backend-summary.md" ||
186
+ name === "frontend-summary.md" ||
187
+ name.endsWith("-context.md");
188
+ }
189
+
190
+ function rootsFromPaths(paths) {
191
+ return Array.from(new Set((paths || [])
192
+ .map((p) => String(p || "").replace(/\\/g, "/").split("/").filter(Boolean).slice(0, 2).join("/"))
193
+ .filter(Boolean)))
194
+ .sort();
195
+ }
196
+
197
+ function sourceRootsForPack(ctx, name) {
198
+ if (name === "backend-summary.md") {
199
+ return rootsFromPaths([
200
+ ...ctx.backend.api,
201
+ ...ctx.backend.services,
202
+ ...ctx.backend.models,
203
+ ...ctx.backend.db,
204
+ ...ctx.backend.auth,
205
+ ...ctx.backend.config,
206
+ ]);
207
+ }
208
+ if (name === "frontend-summary.md") {
209
+ return rootsFromPaths([
210
+ ...ctx.frontend.app,
211
+ ...ctx.frontend.components,
212
+ ...ctx.frontend.apiClient,
213
+ ...ctx.frontend.state,
214
+ ...ctx.frontend.styling,
215
+ ]);
216
+ }
217
+ if (name.endsWith("-context.md")) {
218
+ return rootsFromPaths(scopedRelevantFiles(ctx, name.replace(/-context\.md$/, "")));
219
+ }
220
+ return ctx.folders.slice(0, 30);
221
+ }
222
+
223
+ function matchingSourceFiles(ctx, roots) {
224
+ if (!roots || !roots.length) return ctx.scan.files.slice(0, 250);
225
+ return ctx.scan.files.filter((file) => roots.some((root) => file.path === root || file.path.startsWith(`${root}/`)));
226
+ }
227
+
228
+ function renderPackReceipt(ctx, name, contents) {
229
+ const packPath = `.prismo/${name}`;
230
+ const packId = name.replace(/\.md$/, "");
231
+ const candidateRoots = sourceRootsForPack(ctx, name);
232
+ // Separate roots that actually grounded the pack from roots that were present
233
+ // but deliberately kept out (build output, logs, vendored deps). Without this,
234
+ // a later reader could assume the pack was grounded in e.g. dist/ or logs/.
235
+ const includedSourceRoots = candidateRoots.filter((root) => !isExcludedRoot(root));
236
+ const excludedRoots = candidateRoots.filter((root) => isExcludedRoot(root));
237
+ const sourceFiles = matchingSourceFiles(ctx, includedSourceRoots)
238
+ .filter((file) => file.kind !== "binary")
239
+ .map((file) => ({
240
+ path: file.path,
241
+ size: file.size,
242
+ kind: file.kind,
243
+ tokens: file.tokens,
244
+ }))
245
+ .sort((a, b) => a.path.localeCompare(b.path));
246
+ return JSON.stringify({
247
+ schema_version: 1,
248
+ pack_id: packId,
249
+ pack_path: packPath,
250
+ receipt_path: `.prismo/${packId}.receipt.json`,
251
+ generated_at: ctx.generatedAt,
252
+ prismo_version: PACKAGE_VERSION,
253
+ repo_ref: repoRef(ctx.root),
254
+ target_agents: ["claude-code", "cursor", "codex", "mcp"],
255
+ included_source_roots: includedSourceRoots,
256
+ excluded_roots: excludedRoots,
257
+ source_globs_digest: sha256(JSON.stringify({
258
+ pack_id: packId,
259
+ source_roots: includedSourceRoots,
260
+ files: sourceFiles,
261
+ })),
262
+ omitted_classes: ["lockfiles", "build-output", "large-logs", "coverage", "dependency-vendor", "binary-media"],
263
+ token_budget: 12000,
264
+ estimated_pack_tokens: estimateTokens(contents),
265
+ stale_if_older_than_ms: 86400000,
266
+ verify_command: `${NPX_COMMAND} optimize${ctx.scope ? ` ${ctx.scope}` : ""}`,
267
+ }, null, 2) + "\n";
268
+ }
269
+
128
270
  function renderArchitectureSummary(ctx) {
129
271
  const apiLayer = ctx.backend.api.slice(0, 6);
130
272
  const dbLayer = ctx.backend.db.slice(0, 6);
@@ -429,9 +571,9 @@ function writeOptimizeGeneratedFile(root, relPath, contents) {
429
571
  return { path: relPath, backupPath: null, replaced: existed };
430
572
  }
431
573
 
432
- function renderScopedContext(ctx, scope) {
574
+ function scopedRelevantFiles(ctx, scope) {
433
575
  const scopeLower = scope.toLowerCase();
434
- const relevant = ctx.scan.files
576
+ return ctx.scan.files
435
577
  .filter((file) => {
436
578
  const rel = file.path.toLowerCase();
437
579
  if (scopeLower === "frontend") return rel.includes("frontend/") || rel.includes("src/app/") || rel.includes("src/components/");
@@ -442,6 +584,10 @@ function renderScopedContext(ctx, scope) {
442
584
  .filter((file) => file.kind !== "binary")
443
585
  .slice(0, 60)
444
586
  .map((file) => file.path);
587
+ }
588
+
589
+ function renderScopedContext(ctx, scope) {
590
+ const relevant = scopedRelevantFiles(ctx, scope);
445
591
 
446
592
  return [
447
593
  `# ${scope.charAt(0).toUpperCase()}${scope.slice(1)} Context`,
@@ -544,7 +690,11 @@ function runOptimize(rootDir = process.cwd(), options = {}) {
544
690
  const pending = getOptimizePendingFiles(ctx);
545
691
 
546
692
  if (options.dryRun) {
547
- const generatedFiles = pending.map(([name]) => path.join(".prismo", name));
693
+ const generatedFiles = pending.flatMap(([name]) => {
694
+ const files = [path.join(".prismo", name)];
695
+ if (isContextPackFile(name)) files.push(path.join(".prismo", `${name.replace(/\.md$/, "")}.receipt.json`));
696
+ return files;
697
+ });
548
698
  generatedFiles.push(".prismo/optimize-report.md");
549
699
  return {
550
700
  root: ctx.root,
@@ -564,6 +714,12 @@ function runOptimize(rootDir = process.cwd(), options = {}) {
564
714
  for (const [name, contents] of pending) {
565
715
  const written = writeOptimizeGeneratedFile(ctx.root, path.join(".prismo", name), contents);
566
716
  generated.push(written.path);
717
+ if (isContextPackFile(name)) {
718
+ const receipt = renderPackReceipt(ctx, name, contents);
719
+ const receiptName = `${name.replace(/\.md$/, "")}.receipt.json`;
720
+ const writtenReceipt = writeOptimizeGeneratedFile(ctx.root, path.join(".prismo", receiptName), receipt);
721
+ generated.push(writtenReceipt.path);
722
+ }
567
723
  }
568
724
  const report = renderOptimizeReport(ctx, [...generated, ".prismo/optimize-report.md"]);
569
725
  const writtenReport = writeOptimizeReport(ctx.root, path.join(".prismo", "optimize-report.md"), report);
@@ -53,12 +53,13 @@ const {
53
53
  fs,
54
54
  path,
55
55
  NPX_COMMAND,
56
+ PACKAGE_VERSION,
57
+ estimateTokens,
56
58
  scanRepo: (...args) => scanRepo(...args),
57
59
  safeReadJson,
58
60
  readIfText,
59
61
  formatBytes: (...args) => formatBytes(...args),
60
62
  color,
61
- writeGeneratedFile: (...args) => writeGeneratedFile(...args),
62
63
  });
63
64
 
64
65
  const {
@@ -588,7 +589,6 @@ const { runCli } = require("./prismo-dev/cli")({
588
589
  buildMultiAgentView,
589
590
  buildSyncPayload,
590
591
  loadConfig,
591
- runFirewall,
592
592
  });
593
593
 
594
594
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "description": "Local AI coding workflow scanner for Codex, Claude Code, Cursor, and token-waste diagnostics.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/shanirsh/prismodev#readme",