linear-github-cli 1.2.1 → 1.3.2

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
@@ -301,7 +301,7 @@ gh prms # Merge with squash and delete branch
301
301
  ### Workflow Overview
302
302
 
303
303
  1. **Create issue** - Use `lg parent/sub` command
304
- 2. **Create branch** - Include issue number (e.g., `feat/LEA-123-task`)
304
+ 2. **Create branch** - Include issue number (e.g., `username/LEA-123-task`)
305
305
  3. **Create draft PR** - Right after branch creation, before work begins
306
306
  - Include Linear issue ID in title (copy with `Cmd + .` in Linear)
307
307
  - Include `solve: #123` or `Closes #123` in body
@@ -1,37 +1,16 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.sanitizeBranchName = sanitizeBranchName;
7
- exports.selectBranchPrefix = selectBranchPrefix;
8
4
  exports.generateBranchName = generateBranchName;
9
5
  exports.extractLinearIssueId = extractLinearIssueId;
10
6
  exports.extractBranchPrefix = extractBranchPrefix;
11
7
  exports.checkUnpushedCommitsOnCurrentBranch = checkUnpushedCommitsOnCurrentBranch;
12
8
  exports.createGitBranch = createGitBranch;
13
9
  const child_process_1 = require("child_process");
14
- const inquirer_1 = __importDefault(require("inquirer"));
15
10
  /**
16
11
  * Valid branch prefix types (must match commit_typed.sh)
17
12
  */
18
13
  const VALID_BRANCH_PREFIXES = ['feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'research'];
19
- /**
20
- * Maps GitHub labels to branch prefix types
21
- * Since GitHub labels are already updated to match the standard list,
22
- * labels should directly map to branch prefixes.
23
- * @param label - GitHub label name (should be one of: feat, fix, chore, docs, refactor, test, research)
24
- * @returns Branch prefix (same as label if valid, otherwise returns label as-is)
25
- */
26
- function mapLabelToBranchPrefix(label) {
27
- const labelLower = label.toLowerCase();
28
- // If label is already a valid branch prefix, return it as-is
29
- if (VALID_BRANCH_PREFIXES.includes(labelLower)) {
30
- return labelLower;
31
- }
32
- // Fallback: return label as-is (shouldn't happen if labels are properly configured)
33
- return labelLower;
34
- }
35
14
  /**
36
15
  * Sanitizes a title string to be used as part of a git branch name
37
16
  * - Converts to lowercase
@@ -65,88 +44,49 @@ function sanitizeBranchName(title) {
65
44
  return sanitized;
66
45
  }
67
46
  /**
68
- * Standard branch prefix options when no labels are available
69
- */
70
- const STANDARD_PREFIXES = [
71
- { name: 'feat', value: 'feat' },
72
- { name: 'fix', value: 'fix' },
73
- { name: 'chore', value: 'chore' },
74
- { name: 'docs', value: 'docs' },
75
- { name: 'refactor', value: 'refactor' },
76
- { name: 'test', value: 'test' },
77
- { name: 'research', value: 'research' },
78
- ];
79
- /**
80
- * Selects a branch prefix from GitHub labels
81
- * - If no labels: prompts user to select from standard prefixes
82
- * - If one label: maps and returns branch prefix
83
- * - If multiple labels: prompts user to select one
47
+ * Sanitizes a branch owner (username) to be used in a git branch name.
48
+ * - Converts to lowercase
49
+ * - Removes special characters (keeps alphanumeric and hyphens)
50
+ * - Collapses multiple consecutive hyphens
51
+ * - Removes leading/trailing hyphens
84
52
  *
85
- * @param labels - Array of GitHub label names
86
- * @returns Branch prefix string or null if user cancels
53
+ * @param owner - The branch owner to sanitize
54
+ * @returns Sanitized branch owner portion
87
55
  */
88
- async function selectBranchPrefix(labels) {
89
- // Map all labels to branch prefixes
90
- const mappedPrefixes = labels && labels.length > 0
91
- ? labels.map(label => ({
92
- label,
93
- prefix: mapLabelToBranchPrefix(label),
94
- }))
95
- : [];
96
- // If no labels: prompt user to select from standard prefixes
97
- if (mappedPrefixes.length === 0) {
98
- const { selectedPrefix } = await inquirer_1.default.prompt([
99
- {
100
- type: 'list',
101
- name: 'selectedPrefix',
102
- message: 'Select branch prefix (no labels selected):',
103
- choices: STANDARD_PREFIXES,
104
- },
105
- ]);
106
- return selectedPrefix;
107
- }
108
- // If only one label, return its mapped prefix
109
- if (mappedPrefixes.length === 1) {
110
- return mappedPrefixes[0].prefix;
56
+ function sanitizeBranchOwner(owner) {
57
+ if (!owner || owner.trim().length === 0) {
58
+ return '';
111
59
  }
112
- // Multiple labels: prompt user to select
113
- const choices = mappedPrefixes.map(({ label, prefix }) => ({
114
- name: `${label} ${prefix}`,
115
- value: prefix,
116
- }));
117
- const { selectedPrefix } = await inquirer_1.default.prompt([
118
- {
119
- type: 'list',
120
- name: 'selectedPrefix',
121
- message: 'Select branch prefix (multiple labels selected):',
122
- choices,
123
- },
124
- ]);
125
- return selectedPrefix;
60
+ let sanitized = owner.toLowerCase().replace(/[^a-z0-9-]/g, '');
61
+ sanitized = sanitized.replace(/-+/g, '-');
62
+ sanitized = sanitized.replace(/^-+|-+$/g, '');
63
+ return sanitized;
126
64
  }
127
65
  /**
128
- * Generates a branch name from prefix, Linear ID, and title
129
- * Format: prefix/LinearID-sanitized-title
66
+ * Generates a branch name from owner, Linear ID, and title
67
+ * Format: owner/LinearID-sanitized-title
130
68
  *
131
- * @param prefix - Branch prefix (e.g., 'feat', 'docs')
69
+ * @param owner - Branch owner (e.g., GitHub username)
132
70
  * @param linearId - Linear issue ID (e.g., 'LEA-123')
133
71
  * @param title - Issue title to sanitize
134
72
  * @returns Full branch name
135
73
  */
136
- function generateBranchName(prefix, linearId, title) {
74
+ function generateBranchName(owner, linearId, title) {
75
+ const sanitizedOwner = sanitizeBranchOwner(owner);
137
76
  const sanitizedTitle = sanitizeBranchName(title);
77
+ const ownerSegment = sanitizedOwner || 'user';
138
78
  if (!sanitizedTitle) {
139
- // If title is empty after sanitization, just use prefix and ID
140
- return `${prefix}/${linearId}`;
79
+ // If title is empty after sanitization, just use owner and ID
80
+ return `${ownerSegment}/${linearId}`;
141
81
  }
142
- return `${prefix}/${linearId}-${sanitizedTitle}`;
82
+ return `${ownerSegment}/${linearId}-${sanitizedTitle}`;
143
83
  }
144
84
  /**
145
85
  * Extracts Linear issue ID from a branch name
146
- * - Matches pattern: prefix/LEA-123-title or prefix/LEA-123
86
+ * - Matches pattern: owner/LEA-123-title or owner/LEA-123
147
87
  * - Uses regex to find Linear issue ID format: [A-Z]+-\d+
148
88
  *
149
- * @param branchName - Branch name (e.g., 'feat/LEA-123-implement-login')
89
+ * @param branchName - Branch name (e.g., 'negoth/LEA-123-implement-login')
150
90
  * @returns Linear issue ID (e.g., 'LEA-123') or null if not found
151
91
  */
152
92
  function extractLinearIssueId(branchName) {
@@ -161,27 +101,25 @@ function extractLinearIssueId(branchName) {
161
101
  * Extracts branch prefix (commit type) from a branch name
162
102
  * - Extracts the part before the first '/' (e.g., 'research' from 'research/LEA-75-probit-model')
163
103
  * - Validates against VALID_BRANCH_PREFIXES
164
- * - Returns 'feat' as default if prefix is not found or invalid
165
104
  *
166
105
  * @param branchName - Branch name (e.g., 'research/LEA-75-probit-model')
167
- * @returns Branch prefix (e.g., 'research') or 'feat' as default
106
+ * @returns Branch prefix (e.g., 'research') or null if not found/invalid
168
107
  */
169
108
  function extractBranchPrefix(branchName) {
170
109
  if (!branchName || branchName.trim().length === 0) {
171
- return 'feat';
110
+ return null;
172
111
  }
173
112
  // Extract prefix before first '/'
174
113
  const parts = branchName.split('/');
175
114
  if (parts.length === 0 || !parts[0]) {
176
- return 'feat';
115
+ return null;
177
116
  }
178
117
  const prefix = parts[0].toLowerCase();
179
118
  // Validate against valid prefixes
180
119
  if (VALID_BRANCH_PREFIXES.includes(prefix)) {
181
120
  return prefix;
182
121
  }
183
- // Return 'feat' as default if prefix is invalid
184
- return 'feat';
122
+ return null;
185
123
  }
186
124
  /**
187
125
  * Checks for unpushed commits on the current branch
package/dist/cli.js CHANGED
@@ -13,9 +13,16 @@ const create_sub_1 = require("./commands/create-sub");
13
13
  const env_utils_1 = require("./env-utils");
14
14
  // Load .env file from current working directory, parent directories, or home directory
15
15
  (0, env_utils_1.loadEnvFile)();
16
- // Check for updates
17
- const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)(__dirname, '..', 'package.json'), 'utf-8'));
18
- (0, update_notifier_1.default)({ pkg }).notify();
16
+ // Check for updates (support both dev and built paths)
17
+ const pkgPathCandidates = [
18
+ (0, path_1.resolve)(__dirname, 'package.json'),
19
+ (0, path_1.resolve)(__dirname, '..', 'package.json'),
20
+ ];
21
+ const pkgPath = pkgPathCandidates.find(candidate => (0, fs_1.existsSync)(candidate));
22
+ if (pkgPath) {
23
+ const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8'));
24
+ (0, update_notifier_1.default)({ pkg }).notify();
25
+ }
19
26
  const program = new commander_1.Command();
20
27
  program
21
28
  .name('lg')
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.commitFirst = commitFirst;
4
7
  const child_process_1 = require("child_process");
8
+ const inquirer_1 = __importDefault(require("inquirer"));
5
9
  const branch_utils_1 = require("../branch-utils");
6
10
  const linear_client_1 = require("../linear-client");
7
11
  const env_utils_1 = require("../env-utils");
@@ -37,14 +41,25 @@ async function commitFirst() {
37
41
  console.error('❌ Error: Not in a git repository or unable to get branch name');
38
42
  process.exit(1);
39
43
  }
40
- // Step 2: Extract branch prefix and Linear issue ID from branch name
41
- const prefix = (0, branch_utils_1.extractBranchPrefix)(branchName);
44
+ // Step 2: Extract branch prefix (if any) and Linear issue ID from branch name
45
+ let prefix = (0, branch_utils_1.extractBranchPrefix)(branchName);
42
46
  const linearId = (0, branch_utils_1.extractLinearIssueId)(branchName);
43
47
  if (!linearId) {
44
48
  console.error(`❌ Error: Could not extract Linear issue ID from branch name: ${branchName}`);
45
- console.error(' Branch name should follow pattern: prefix/LEA-123-title');
49
+ console.error(' Branch name should follow pattern: username/LEA-123-title');
46
50
  process.exit(1);
47
51
  }
52
+ if (!prefix) {
53
+ const { selectedPrefix } = await inquirer_1.default.prompt([
54
+ {
55
+ type: 'list',
56
+ name: 'selectedPrefix',
57
+ message: 'Select commit type:',
58
+ choices: ['feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'research'],
59
+ },
60
+ ]);
61
+ prefix = selectedPrefix;
62
+ }
48
63
  console.log(`📋 Found Linear issue ID: ${linearId}`);
49
64
  console.log(`📋 Using commit type: ${prefix}`);
50
65
  // Step 3: Initialize Linear client and fetch issue data
@@ -83,22 +83,30 @@ async function createParentIssue() {
83
83
  });
84
84
  if (linearIssueId) {
85
85
  console.log('✅ Found Linear issue, updating metadata...');
86
- // Auto-find Linear project if GitHub project was selected
86
+ // Auto-find/create Linear project if GitHub project was selected
87
87
  let linearProjectId = null;
88
+ let linearProjectName = null;
88
89
  if (githubProject) {
89
90
  console.log(` Looking for Linear project matching "${githubProject}"...`);
90
91
  linearProjectId = await linearClient.findProjectByName(githubProject);
91
92
  if (linearProjectId) {
93
+ linearProjectName = githubProject;
92
94
  console.log(` ✅ Found matching Linear project: ${githubProject}`);
93
95
  }
94
96
  else {
95
- console.log(` ⚠️ No matching Linear project found. You can set it manually.`);
97
+ console.log(` ⚠️ No matching Linear project found. Creating "${githubProject}"...`);
98
+ const teamId = await linearClient.getIssueTeamId(linearIssueId);
99
+ const createdProject = await linearClient.createProject(githubProject, teamId || undefined);
100
+ if (createdProject) {
101
+ linearProjectId = createdProject.id;
102
+ linearProjectName = createdProject.name;
103
+ console.log(` ✅ Created Linear project: ${createdProject.name}`);
104
+ }
105
+ else {
106
+ console.log(` ⚠️ Failed to create Linear project. You can set it manually.`);
107
+ }
96
108
  }
97
109
  }
98
- // If no auto-match, ask user (only if GitHub project wasn't selected)
99
- if (!linearProjectId && !githubProject) {
100
- linearProjectId = await inputHandler.selectLinearProject();
101
- }
102
110
  // Set labels on Linear issue
103
111
  if (details.labels && details.labels.length > 0) {
104
112
  console.log(` Setting labels: ${details.labels.join(', ')}`);
@@ -115,7 +123,7 @@ async function createParentIssue() {
115
123
  if (success) {
116
124
  console.log('✅ Linear issue metadata updated!');
117
125
  if (linearProjectId) {
118
- console.log(` Project: ${githubProject || 'selected project'}`);
126
+ console.log(` Project: ${linearProjectName || githubProject || 'linked'}`);
119
127
  }
120
128
  if (details.dueDate) {
121
129
  console.log(` Due date: ${details.dueDate}`);
@@ -144,18 +152,22 @@ async function createParentIssue() {
144
152
  console.log(` GitHub issue #${issue.number}`);
145
153
  }
146
154
  else {
147
- const branchPrefix = await (0, branch_utils_1.selectBranchPrefix)(details.labels);
148
- if (branchPrefix) {
149
- const branchName = (0, branch_utils_1.generateBranchName)(branchPrefix, linearIssueIdentifier, details.title);
150
- const success = await (0, branch_utils_1.createGitBranch)(branchName);
151
- if (success) {
152
- console.log(`✅ Branch created: ${branchName}`);
153
- console.log(` Linear issue ID: ${linearIssueIdentifier}`);
154
- console.log(` GitHub issue #${issue.number}`);
155
- }
155
+ let branchOwner = await githubClient.getCurrentUsername();
156
+ if (!branchOwner) {
157
+ const { ownerInput } = await inquirer_1.default.prompt([
158
+ {
159
+ type: 'input',
160
+ name: 'ownerInput',
161
+ message: 'Branch username for naming (e.g., your GitHub login):',
162
+ validate: (input) => input.trim().length > 0 || 'Username is required',
163
+ },
164
+ ]);
165
+ branchOwner = ownerInput.trim();
156
166
  }
157
- else {
158
- console.log('⚠️ No suitable label found for branch prefix. Branch creation skipped.');
167
+ const branchName = (0, branch_utils_1.generateBranchName)(branchOwner ?? 'user', linearIssueIdentifier, details.title);
168
+ const success = await (0, branch_utils_1.createGitBranch)(branchName);
169
+ if (success) {
170
+ console.log(`✅ Branch created: ${branchName}`);
159
171
  console.log(` Linear issue ID: ${linearIssueIdentifier}`);
160
172
  console.log(` GitHub issue #${issue.number}`);
161
173
  }
@@ -110,10 +110,7 @@ async function createSubIssue() {
110
110
  else {
111
111
  console.log(` ⚠️ Parent Linear issue not found yet (may need more time to sync)`);
112
112
  }
113
- // If no parent project, ask user (only if no parent project found)
114
- if (!linearProjectId) {
115
- linearProjectId = await inputHandler.selectLinearProject();
116
- }
113
+ // If no parent project, leave project unset (no prompt)
117
114
  // Set labels on Linear issue
118
115
  if (details.labels && details.labels.length > 0) {
119
116
  console.log(` Setting labels: ${details.labels.join(', ')}`);
@@ -172,18 +169,22 @@ async function createSubIssue() {
172
169
  console.log(` GitHub issue #${subIssue.number}`);
173
170
  }
174
171
  else {
175
- const branchPrefix = await (0, branch_utils_1.selectBranchPrefix)(details.labels);
176
- if (branchPrefix) {
177
- const branchName = (0, branch_utils_1.generateBranchName)(branchPrefix, linearIssueIdentifier, details.title);
178
- const success = await (0, branch_utils_1.createGitBranch)(branchName);
179
- if (success) {
180
- console.log(`✅ Branch created: ${branchName}`);
181
- console.log(` Linear issue ID: ${linearIssueIdentifier}`);
182
- console.log(` GitHub issue #${subIssue.number}`);
183
- }
172
+ let branchOwner = await githubClient.getCurrentUsername();
173
+ if (!branchOwner) {
174
+ const { ownerInput } = await inquirer_1.default.prompt([
175
+ {
176
+ type: 'input',
177
+ name: 'ownerInput',
178
+ message: 'Branch username for naming (e.g., your GitHub login):',
179
+ validate: (input) => input.trim().length > 0 || 'Username is required',
180
+ },
181
+ ]);
182
+ branchOwner = ownerInput.trim();
184
183
  }
185
- else {
186
- console.log('⚠️ No suitable label found for branch prefix. Branch creation skipped.');
184
+ const branchName = (0, branch_utils_1.generateBranchName)(branchOwner ?? 'user', linearIssueIdentifier, details.title);
185
+ const success = await (0, branch_utils_1.createGitBranch)(branchName);
186
+ if (success) {
187
+ console.log(`✅ Branch created: ${branchName}`);
187
188
  console.log(` Linear issue ID: ${linearIssueIdentifier}`);
188
189
  console.log(` GitHub issue #${subIssue.number}`);
189
190
  }
package/dist/env-utils.js CHANGED
@@ -14,6 +14,22 @@ const os_1 = require("os");
14
14
  * - Installed globally (user can create .env in their project or home directory)
15
15
  * - Installed locally (works with project's .env file)
16
16
  */
17
+ function hasLinearApiKey(envPath) {
18
+ const contents = (0, fs_1.readFileSync)(envPath, 'utf-8');
19
+ const match = contents.match(/^\s*LINEAR_API_KEY\s*=\s*(.+)\s*$/m);
20
+ if (!match) {
21
+ return false;
22
+ }
23
+ const rawValue = match[1].trim();
24
+ const unquoted = rawValue.replace(/^['"]|['"]$/g, '').trim();
25
+ return unquoted.length > 0;
26
+ }
27
+ function exitMissingLinearApiKey() {
28
+ console.error('❌ LINEAR_API_KEY not found in .env');
29
+ console.error(' Add it to your .env file:');
30
+ console.error(' echo "LINEAR_API_KEY=lin_api_..." > .env');
31
+ process.exit(1);
32
+ }
17
33
  function loadEnvFile() {
18
34
  // First, try to find .env in current working directory and parent directories
19
35
  let currentDir = process.cwd();
@@ -21,6 +37,10 @@ function loadEnvFile() {
21
37
  while (currentDir !== root) {
22
38
  const envPath = (0, path_1.resolve)(currentDir, '.env');
23
39
  if ((0, fs_1.existsSync)(envPath)) {
40
+ if (!hasLinearApiKey(envPath)) {
41
+ exitMissingLinearApiKey();
42
+ }
43
+ console.log(`✅ LINEAR_API_KEY found in ${envPath}`);
24
44
  (0, dotenv_1.config)({ path: envPath });
25
45
  return;
26
46
  }
@@ -34,10 +54,12 @@ function loadEnvFile() {
34
54
  // Fallback: try home directory
35
55
  const homeEnvPath = (0, path_1.resolve)((0, os_1.homedir)(), '.env');
36
56
  if ((0, fs_1.existsSync)(homeEnvPath)) {
57
+ if (!hasLinearApiKey(homeEnvPath)) {
58
+ exitMissingLinearApiKey();
59
+ }
60
+ console.log(`✅ LINEAR_API_KEY found in ${homeEnvPath}`);
37
61
  (0, dotenv_1.config)({ path: homeEnvPath });
38
62
  return;
39
63
  }
40
- // If no .env file found, dotenv will use environment variables
41
- // This is fine - we don't need to throw an error here
42
- (0, dotenv_1.config)();
64
+ exitMissingLinearApiKey();
43
65
  }
@@ -12,6 +12,18 @@ class GitHubClientWrapper {
12
12
  this.octokit = new rest_1.Octokit({ auth: token });
13
13
  }
14
14
  }
15
+ async getCurrentUsername() {
16
+ try {
17
+ const output = (0, child_process_1.execSync)('gh api user -q .login', {
18
+ encoding: 'utf-8',
19
+ stdio: 'pipe',
20
+ }).trim();
21
+ return output || null;
22
+ }
23
+ catch (error) {
24
+ return null;
25
+ }
26
+ }
15
27
  async getRepositories() {
16
28
  // Use gh CLI to get accessible repos
17
29
  const output = (0, child_process_1.execSync)('gh repo list --limit 100 --json nameWithOwner', {
@@ -187,11 +187,8 @@ class InputHandler {
187
187
  {
188
188
  type: 'checkbox',
189
189
  name: 'labels',
190
- message: 'Select GitHub labels (at least one required):',
190
+ message: 'Select GitHub labels (optional):',
191
191
  choices: labelChoices,
192
- validate: (input) => {
193
- return input.length > 0 || 'At least one label is required';
194
- },
195
192
  },
196
193
  ]);
197
194
  let description = '';
@@ -28,6 +28,41 @@ class LinearClientWrapper {
28
28
  return project?.id || null;
29
29
  }
30
30
  }
31
+ async createProject(projectName, teamId) {
32
+ try {
33
+ const mutation = `mutation CreateProject($input: ProjectCreateInput!) {
34
+ projectCreate(input: $input) {
35
+ success
36
+ project { id name }
37
+ }
38
+ }`;
39
+ const input = { name: projectName };
40
+ if (teamId) {
41
+ input.teamIds = [teamId];
42
+ }
43
+ const response = await fetch('https://api.linear.app/graphql', {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ 'Authorization': linearApiKey,
48
+ },
49
+ body: JSON.stringify({
50
+ query: mutation,
51
+ variables: { input },
52
+ }),
53
+ });
54
+ const result = await response.json();
55
+ if (result.errors || !result.data?.projectCreate?.success) {
56
+ console.error(`❌ Failed to create project "${projectName}":`, result.errors);
57
+ return null;
58
+ }
59
+ return result.data.projectCreate.project ?? null;
60
+ }
61
+ catch (error) {
62
+ console.error(`❌ Error creating project "${projectName}":`, error);
63
+ return null;
64
+ }
65
+ }
31
66
  async getWorkflowStates(teamId) {
32
67
  try {
33
68
  // Get workflow states from teams
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-github-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.2",
4
4
  "description": "CLI tool for creating GitHub issues with Linear integration",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {