diffprism 0.13.7 → 0.14.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/dist/bin.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  readWatchFile,
4
4
  startReview,
5
5
  startWatch
6
- } from "./chunk-QB2PKDLU.js";
6
+ } from "./chunk-TVXIMP3G.js";
7
7
 
8
8
  // cli/src/index.ts
9
9
  import { Command } from "commander";
@@ -18,7 +18,7 @@ async function review(ref, flags) {
18
18
  } else if (ref) {
19
19
  diffRef = ref;
20
20
  } else {
21
- diffRef = "all";
21
+ diffRef = "working-copy";
22
22
  }
23
23
  try {
24
24
  const result = await startReview({
@@ -66,57 +66,29 @@ Before opening a new review, check if \`diffprism watch\` is already running. Lo
66
66
 
67
67
  - **Do NOT call \`open_review\`** (the browser is already open with live-updating diffs)
68
68
  - Instead, call \`mcp__diffprism__update_review_context\` to push your reasoning to the existing watch session
69
- - Tell the user: "DiffPrism watch is running \u2014 pushed reasoning to the live review."
70
- - Skip the remaining steps
69
+ - Then **immediately** call \`mcp__diffprism__get_review_result\` with \`wait: true\` to block until the developer submits their review
70
+ - Tell the user: "DiffPrism watch is running \u2014 pushed reasoning to the live review. Waiting for your feedback..."
71
+ - When the result comes back, handle it per step 5 below
72
+ - Skip steps 2-4
71
73
 
72
- ### 1b. Check for Pending Review Feedback
74
+ ### 2. Load Configuration
73
75
 
74
- If watch mode is running, call \`mcp__diffprism__get_review_result\` to check for pending review feedback from the developer. If a result is returned:
75
-
76
- - **\`approved\`** \u2014 Acknowledge approval and continue with your current task.
77
- - **\`approved_with_comments\`** \u2014 Note the comments, address any actionable feedback.
78
- - **\`changes_requested\`** \u2014 Read the comments carefully, make the requested changes, then push updated reasoning via \`mcp__diffprism__update_review_context\`.
79
-
80
- If no pending result, continue normally.
81
-
82
- ### 2. Check for Configuration
83
-
84
- Look for \`diffprism.config.json\` at the project root. If it exists, read it for preferences:
76
+ Look for \`diffprism.config.json\` at the project root. If it exists, read it for preferences. If it doesn't exist, use defaults silently \u2014 do not prompt or create the file.
85
77
 
86
78
  \`\`\`json
87
79
  {
88
80
  "reviewTrigger": "ask | before_commit | always",
89
- "defaultDiffScope": "staged | unstaged | all",
81
+ "defaultDiffScope": "staged | unstaged | working-copy",
90
82
  "includeReasoning": true | false
91
83
  }
92
84
  \`\`\`
93
85
 
94
86
  **Defaults** (when fields are missing or file doesn't exist):
95
87
  - \`reviewTrigger\`: \`"ask"\`
96
- - \`defaultDiffScope\`: \`"all"\`
88
+ - \`defaultDiffScope\`: \`"working-copy"\`
97
89
  - \`includeReasoning\`: \`true\`
98
90
 
99
- ### 3. First-Run Onboarding
100
-
101
- If \`diffprism.config.json\` does **not** exist, ask the user these questions before proceeding:
102
-
103
- 1. **"When should I open DiffPrism reviews?"**
104
- - \`"ask"\` \u2014 Only when you explicitly ask (default)
105
- - \`"before_commit"\` \u2014 Automatically before every commit
106
- - \`"always"\` \u2014 After every code change
107
-
108
- 2. **"What should the default diff scope be?"**
109
- - \`"all"\` \u2014 All changes, staged and unstaged (default)
110
- - \`"staged"\` \u2014 Only staged changes
111
- - \`"unstaged"\` \u2014 Only unstaged changes
112
-
113
- 3. **"Should I include my reasoning about the changes in reviews?"**
114
- - Yes (default)
115
- - No
116
-
117
- After collecting answers, create \`diffprism.config.json\` at the project root with the user's choices. Then proceed to open the review.
118
-
119
- ### 4. Open the Review
91
+ ### 3. Open the Review
120
92
 
121
93
  Call \`mcp__diffprism__open_review\` with:
122
94
 
@@ -125,7 +97,7 @@ Call \`mcp__diffprism__open_review\` with:
125
97
  - \`description\`: A brief description of what changed and why.
126
98
  - \`reasoning\`: If \`includeReasoning\` is \`true\`, include your reasoning about the implementation decisions.
127
99
 
128
- ### 5. Handle the Result
100
+ ### 4. Handle the Result
129
101
 
130
102
  The tool blocks until the user submits their review in the browser. When it returns:
131
103
 
@@ -133,21 +105,24 @@ The tool blocks until the user submits their review in the browser. When it retu
133
105
  - **\`approved_with_comments\`** \u2014 Note the comments, address any actionable feedback.
134
106
  - **\`changes_requested\`** \u2014 Read the comments carefully, make the requested changes, and offer to open another review.
135
107
 
136
- ### 6. Error Handling
108
+ ### 5. Error Handling
137
109
 
138
110
  If the \`mcp__diffprism__open_review\` tool is not available:
139
- - Tell the user: "The DiffPrism MCP server isn't configured. Run \`npx diffprism setup\` to set it up, then restart Claude Code."
111
+ - Tell the user: "The DiffPrism MCP server isn't configured. Run \`npx diffprism start\` to set it up, then restart Claude Code."
140
112
 
141
- ## Watch Mode: Polling for Review Feedback
113
+ ## Watch Mode: Waiting for Review Feedback
142
114
 
143
- When \`diffprism watch\` is active (detected via \`.diffprism/watch.json\`), the developer can submit reviews at any time in the browser. Since there is no push notification, **you must poll for feedback** to close the loop.
115
+ When \`diffprism watch\` is active (detected via \`.diffprism/watch.json\`), the developer can submit reviews at any time in the browser.
144
116
 
145
- **After pushing context to a watch session**, call \`mcp__diffprism__get_review_result\` to check for pending feedback:
146
- - **Between tasks** \u2014 Before starting a new piece of work, check for feedback.
147
- - **After making changes** \u2014 After addressing requested changes, push updated reasoning via \`update_review_context\`, then check again shortly after.
148
- - **When the user mentions review feedback** \u2014 If the user says they submitted a review or left comments, check immediately.
117
+ **After pushing context to a watch session**, call \`mcp__diffprism__get_review_result\` with \`wait: true\` to block until the developer submits their review. This polls the result file every 2 seconds and returns as soon as feedback is available (up to 5 minutes by default).
149
118
 
150
- Do not poll in a tight loop. Check at natural breakpoints in your workflow (e.g., after finishing a subtask, before committing, before moving to the next file).
119
+ Use this pattern:
120
+ 1. Push context via \`update_review_context\`
121
+ 2. Call \`get_review_result\` with \`wait: true\` \u2014 this blocks until the developer submits
122
+ 3. Handle the result (approved, changes_requested, etc.)
123
+ 4. If changes were requested, make fixes, push updated context, and call \`get_review_result\` with \`wait: true\` again
124
+
125
+ You can also check for feedback without blocking by calling \`get_review_result\` without \`wait\` at natural breakpoints (between tasks, before committing, etc.).
151
126
 
152
127
  ## Behavior Rules
153
128
 
@@ -156,7 +131,7 @@ Do not poll in a tight loop. Check at natural breakpoints in your workflow (e.g.
156
131
  - \`"ask"\` \u2014 Never auto-review; only review when the user asks.
157
132
  - \`"before_commit"\` \u2014 Open a review before creating any git commit.
158
133
  - \`"always"\` \u2014 Open a review after any code change.
159
- - To re-run onboarding, the user can delete \`diffprism.config.json\` and invoke \`/review\` again.
134
+ - Power users can create \`diffprism.config.json\` manually to customize defaults.
160
135
  `;
161
136
 
162
137
  // cli/src/commands/setup.ts
@@ -247,6 +222,34 @@ function setupSkill(gitRoot, global, force) {
247
222
  fs.writeFileSync(filePath, skillContent);
248
223
  return { action, filePath };
249
224
  }
225
+ function cleanDiffprismHooks(gitRoot) {
226
+ const filePath = path.join(gitRoot, ".claude", "settings.json");
227
+ const existing = readJsonFile(filePath);
228
+ const hooks = existing.hooks;
229
+ if (!hooks) return { removed: 0 };
230
+ const stopHooks = hooks.Stop;
231
+ if (!Array.isArray(stopHooks) || stopHooks.length === 0) {
232
+ return { removed: 0 };
233
+ }
234
+ const filtered = stopHooks.filter((entry) => {
235
+ const innerHooks = entry.hooks;
236
+ if (!Array.isArray(innerHooks)) return true;
237
+ return !innerHooks.some((h) => {
238
+ const cmd = h.command;
239
+ return typeof cmd === "string" && cmd.includes("diffprism") && cmd.includes("notify-stop");
240
+ });
241
+ });
242
+ const removed = stopHooks.length - filtered.length;
243
+ if (removed > 0) {
244
+ if (filtered.length > 0) {
245
+ hooks.Stop = filtered;
246
+ } else {
247
+ delete hooks.Stop;
248
+ }
249
+ writeJsonFile(filePath, { ...existing, hooks });
250
+ }
251
+ return { removed };
252
+ }
250
253
  function setupStopHook(gitRoot, force) {
251
254
  const filePath = path.join(gitRoot, ".claude", "settings.json");
252
255
  const existing = readJsonFile(filePath);
@@ -324,11 +327,14 @@ async function setup(flags) {
324
327
  "Error: Not in a git repository. Run this command from inside a git project."
325
328
  );
326
329
  process.exit(1);
327
- return;
330
+ return { created: [], updated: [], skipped: [] };
328
331
  }
329
332
  const force = flags.force ?? false;
330
333
  const global = flags.global ?? false;
331
- console.log("Setting up DiffPrism for Claude Code...\n");
334
+ const quiet = flags.quiet ?? false;
335
+ if (!quiet) {
336
+ console.log("Setting up DiffPrism for Claude Code...\n");
337
+ }
332
338
  const result = { created: [], updated: [], skipped: [] };
333
339
  const gitignore = await setupGitignore(gitRoot);
334
340
  result[gitignore.action].push(gitignore.filePath);
@@ -336,37 +342,92 @@ async function setup(flags) {
336
342
  result[mcp.action === "skipped" ? "skipped" : mcp.action === "created" ? "created" : "updated"].push(mcp.filePath);
337
343
  const settings = setupClaudeSettings(gitRoot, force);
338
344
  result[settings.action === "skipped" ? "skipped" : settings.action === "created" ? "created" : "updated"].push(settings.filePath);
345
+ const cleaned = cleanDiffprismHooks(gitRoot);
346
+ if (cleaned.removed > 0 && !quiet) {
347
+ console.log(` Cleaned ${cleaned.removed} stale hook(s)`);
348
+ }
339
349
  const hook = setupStopHook(gitRoot, force);
340
350
  result[hook.action === "skipped" ? "skipped" : hook.action === "created" ? "created" : "updated"].push(hook.filePath);
341
351
  const skill = setupSkill(gitRoot, global, force);
342
352
  result[skill.action === "skipped" ? "skipped" : skill.action === "created" ? "created" : "updated"].push(skill.filePath);
343
- if (result.created.length > 0) {
344
- console.log("Created:");
345
- for (const f of result.created) {
346
- console.log(` + ${path.relative(gitRoot, f)}`);
353
+ if (!quiet) {
354
+ if (result.created.length > 0) {
355
+ console.log("Created:");
356
+ for (const f of result.created) {
357
+ console.log(` + ${path.relative(gitRoot, f)}`);
358
+ }
347
359
  }
348
- }
349
- if (result.updated.length > 0) {
350
- console.log("Updated:");
351
- for (const f of result.updated) {
352
- console.log(` ~ ${path.relative(gitRoot, f)}`);
360
+ if (result.updated.length > 0) {
361
+ console.log("Updated:");
362
+ for (const f of result.updated) {
363
+ console.log(` ~ ${path.relative(gitRoot, f)}`);
364
+ }
353
365
  }
366
+ if (result.skipped.length > 0) {
367
+ console.log("Skipped (already configured):");
368
+ for (const f of result.skipped) {
369
+ console.log(` - ${path.relative(gitRoot, f)}`);
370
+ }
371
+ }
372
+ console.log("\n\u2713 DiffPrism configured for Claude Code.\n");
373
+ console.log("Next steps:");
374
+ console.log(" 1. Restart Claude Code to pick up the MCP configuration");
375
+ console.log(" 2. Use /review in Claude Code to review your changes\n");
376
+ console.log("Tip: Run `diffprism start` to combine setup + live watch mode.");
377
+ }
378
+ return result;
379
+ }
380
+
381
+ // cli/src/commands/start.ts
382
+ async function start(ref, flags) {
383
+ const outcome = await setup({
384
+ global: flags.global,
385
+ force: flags.force,
386
+ quiet: true
387
+ });
388
+ const hasChanges = outcome.created.length > 0 || outcome.updated.length > 0;
389
+ if (hasChanges) {
390
+ console.log("\u2713 DiffPrism configured for Claude Code.");
354
391
  }
355
- if (result.skipped.length > 0) {
356
- console.log("Skipped (already configured):");
357
- for (const f of result.skipped) {
358
- console.log(` - ${path.relative(gitRoot, f)}`);
392
+ let diffRef;
393
+ if (flags.staged) {
394
+ diffRef = "staged";
395
+ } else if (flags.unstaged) {
396
+ diffRef = "unstaged";
397
+ } else if (ref) {
398
+ diffRef = ref;
399
+ } else {
400
+ diffRef = "working-copy";
401
+ }
402
+ const pollInterval = flags.interval ? parseInt(flags.interval, 10) : 1e3;
403
+ try {
404
+ const handle = await startWatch({
405
+ diffRef,
406
+ title: flags.title,
407
+ cwd: process.cwd(),
408
+ dev: flags.dev,
409
+ pollInterval
410
+ });
411
+ console.log("Use /review in Claude Code to send changes for review.");
412
+ if (hasChanges) {
413
+ console.log(
414
+ "If this is your first time, restart Claude Code first to load the MCP server."
415
+ );
359
416
  }
417
+ const shutdown = async () => {
418
+ console.log("\nStopping DiffPrism...");
419
+ await handle.stop();
420
+ process.exit(0);
421
+ };
422
+ process.on("SIGINT", shutdown);
423
+ process.on("SIGTERM", shutdown);
424
+ await new Promise(() => {
425
+ });
426
+ } catch (err) {
427
+ const message = err instanceof Error ? err.message : String(err);
428
+ console.error(`Error: ${message}`);
429
+ process.exit(1);
360
430
  }
361
- console.log(
362
- "\nYou can now use /review in Claude Code to open a DiffPrism review."
363
- );
364
- console.log(
365
- "Or run `diffprism watch --staged` for live-updating reviews."
366
- );
367
- console.log(
368
- "If Claude Code is running, restart it to pick up the new configuration."
369
- );
370
431
  }
371
432
 
372
433
  // cli/src/commands/watch.ts
@@ -379,7 +440,7 @@ async function watch(ref, flags) {
379
440
  } else if (ref) {
380
441
  diffRef = ref;
381
442
  } else {
382
- diffRef = "all";
443
+ diffRef = "working-copy";
383
444
  }
384
445
  const pollInterval = flags.interval ? parseInt(flags.interval, 10) : 1e3;
385
446
  try {
@@ -431,10 +492,13 @@ async function notifyStop() {
431
492
 
432
493
  // cli/src/index.ts
433
494
  var program = new Command();
434
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.13.7" : "0.0.0-dev");
495
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.14.0" : "0.0.0-dev");
435
496
  program.command("review [ref]").description("Open a browser-based diff review").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--dev", "Use Vite dev server with HMR instead of static files").action(review);
497
+ program.command("start [ref]").description("Set up DiffPrism and start watching for changes").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action(start);
436
498
  program.command("watch [ref]").description("Start a persistent diff watcher with live-updating browser UI").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").action(watch);
437
499
  program.command("notify-stop").description("Signal the watch server to refresh (used by Claude Code hooks)").action(notifyStop);
438
500
  program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
439
- program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action(setup);
501
+ program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action((flags) => {
502
+ setup(flags);
503
+ });
440
504
  program.parse();
@@ -303,6 +303,9 @@ function parseDiff(rawDiff, baseRef, headRef) {
303
303
 
304
304
  // packages/git/src/index.ts
305
305
  function getDiff(ref, options) {
306
+ if (ref === "working-copy") {
307
+ return getWorkingCopyDiff(options);
308
+ }
306
309
  const rawDiff = getGitDiff(ref, options);
307
310
  let baseRef;
308
311
  let headRef;
@@ -323,6 +326,29 @@ function getDiff(ref, options) {
323
326
  const diffSet = parseDiff(rawDiff, baseRef, headRef);
324
327
  return { diffSet, rawDiff };
325
328
  }
329
+ function getWorkingCopyDiff(options) {
330
+ const stagedRaw = getGitDiff("staged", options);
331
+ const unstagedRaw = getGitDiff("unstaged", options);
332
+ const stagedDiffSet = parseDiff(stagedRaw, "HEAD", "staged");
333
+ const unstagedDiffSet = parseDiff(unstagedRaw, "staged", "working tree");
334
+ const stagedFiles = stagedDiffSet.files.map((f) => ({
335
+ ...f,
336
+ stage: "staged"
337
+ }));
338
+ const unstagedFiles = unstagedDiffSet.files.map((f) => ({
339
+ ...f,
340
+ stage: "unstaged"
341
+ }));
342
+ const rawDiff = [stagedRaw, unstagedRaw].filter(Boolean).join("");
343
+ return {
344
+ diffSet: {
345
+ baseRef: "HEAD",
346
+ headRef: "working tree",
347
+ files: [...stagedFiles, ...unstagedFiles]
348
+ },
349
+ rawDiff
350
+ };
351
+ }
326
352
 
327
353
  // packages/analysis/src/deterministic.ts
328
354
  function categorizeFiles(files) {
@@ -1173,25 +1199,29 @@ function createWatchBridge(port, callbacks) {
1173
1199
  function hashDiff(rawDiff) {
1174
1200
  return createHash("sha256").update(rawDiff).digest("hex");
1175
1201
  }
1202
+ function fileKey(file) {
1203
+ return file.stage ? `${file.stage}:${file.path}` : file.path;
1204
+ }
1176
1205
  function detectChangedFiles(oldDiffSet, newDiffSet) {
1177
1206
  if (!oldDiffSet) {
1178
- return newDiffSet.files.map((f) => f.path);
1207
+ return newDiffSet.files.map(fileKey);
1179
1208
  }
1180
1209
  const oldFiles = new Map(
1181
- oldDiffSet.files.map((f) => [f.path, f])
1210
+ oldDiffSet.files.map((f) => [fileKey(f), f])
1182
1211
  );
1183
1212
  const changed = [];
1184
1213
  for (const newFile of newDiffSet.files) {
1185
- const oldFile = oldFiles.get(newFile.path);
1214
+ const key = fileKey(newFile);
1215
+ const oldFile = oldFiles.get(key);
1186
1216
  if (!oldFile) {
1187
- changed.push(newFile.path);
1217
+ changed.push(key);
1188
1218
  } else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
1189
- changed.push(newFile.path);
1219
+ changed.push(key);
1190
1220
  }
1191
1221
  }
1192
1222
  for (const oldFile of oldDiffSet.files) {
1193
- if (!newDiffSet.files.some((f) => f.path === oldFile.path)) {
1194
- changed.push(oldFile.path);
1223
+ if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
1224
+ changed.push(fileKey(oldFile));
1195
1225
  }
1196
1226
  }
1197
1227
  return changed;
@@ -3,7 +3,7 @@ import {
3
3
  readReviewResult,
4
4
  readWatchFile,
5
5
  startReview
6
- } from "./chunk-QB2PKDLU.js";
6
+ } from "./chunk-TVXIMP3G.js";
7
7
 
8
8
  // packages/mcp-server/src/index.ts
9
9
  import fs from "fs";
@@ -14,14 +14,14 @@ import { z } from "zod";
14
14
  async function startMcpServer() {
15
15
  const server = new McpServer({
16
16
  name: "diffprism",
17
- version: true ? "0.13.7" : "0.0.0-dev"
17
+ version: true ? "0.14.0" : "0.0.0-dev"
18
18
  });
19
19
  server.tool(
20
20
  "open_review",
21
21
  "Open a browser-based code review for local git changes. Blocks until the engineer submits their review decision.",
22
22
  {
23
23
  diff_ref: z.string().describe(
24
- 'Git diff reference: "staged", "unstaged", or a ref range like "HEAD~3..HEAD"'
24
+ 'Git diff reference: "staged", "unstaged", "working-copy" (staged+unstaged grouped), or a ref range like "HEAD~3..HEAD"'
25
25
  ),
26
26
  title: z.string().optional().describe("Title for the review"),
27
27
  description: z.string().optional().describe("Description of the changes"),
@@ -124,10 +124,41 @@ async function startMcpServer() {
124
124
  );
125
125
  server.tool(
126
126
  "get_review_result",
127
- "Fetch the most recent review result from a DiffPrism watch session. Returns the reviewer's decision and comments if a review has been submitted, or a message indicating no pending result. The result is marked as consumed after retrieval so it won't be returned again.",
128
- {},
129
- async () => {
127
+ "Fetch the most recent review result from a DiffPrism watch session. Returns the reviewer's decision and comments if a review has been submitted, or a message indicating no pending result. The result is marked as consumed after retrieval so it won't be returned again. Use wait=true to block until a result is available (recommended after pushing context to a watch session).",
128
+ {
129
+ wait: z.boolean().optional().describe("If true, poll until a review result is available (blocks up to timeout)"),
130
+ timeout: z.number().optional().describe("Max wait time in seconds when wait=true (default: 300, max: 600)")
131
+ },
132
+ async ({ wait, timeout }) => {
130
133
  try {
134
+ if (wait) {
135
+ const maxWaitMs = Math.min(timeout ?? 300, 600) * 1e3;
136
+ const pollIntervalMs = 2e3;
137
+ const start = Date.now();
138
+ while (Date.now() - start < maxWaitMs) {
139
+ const data2 = readReviewResult();
140
+ if (data2) {
141
+ consumeReviewResult();
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: JSON.stringify(data2.result, null, 2)
147
+ }
148
+ ]
149
+ };
150
+ }
151
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
152
+ }
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: "No review result received within timeout."
158
+ }
159
+ ]
160
+ };
161
+ }
131
162
  const data = readReviewResult();
132
163
  if (!data) {
133
164
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.13.7",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {