yapout 0.5.2 → 0.8.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.
Files changed (2) hide show
  1. package/dist/index.js +422 -116
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28,6 +28,75 @@ import {
28
28
  writeFileSync,
29
29
  unlinkSync
30
30
  } from "fs";
31
+
32
+ // src/lib/git.ts
33
+ import { execSync } from "child_process";
34
+ import { dirname, isAbsolute, resolve } from "path";
35
+ function git(args, cwd) {
36
+ return execSync(`git ${args}`, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
37
+ }
38
+ function resolveRepoRoot(cwd) {
39
+ try {
40
+ const commonDir = git("rev-parse --git-common-dir", cwd);
41
+ const abs = isAbsolute(commonDir) ? commonDir : resolve(cwd, commonDir);
42
+ return dirname(abs);
43
+ } catch {
44
+ return cwd;
45
+ }
46
+ }
47
+ function getRepoFullName(cwd) {
48
+ const url = git("remote get-url origin", cwd);
49
+ const sshMatch = url.match(/git@github\.com:(.+?)(?:\.git)?$/);
50
+ if (sshMatch) return sshMatch[1];
51
+ const httpsMatch = url.match(/github\.com\/(.+?)(?:\.git)?$/);
52
+ if (httpsMatch) return httpsMatch[1];
53
+ throw new Error(`Could not parse GitHub repo from remote URL: ${url}`);
54
+ }
55
+ function getDefaultBranch(cwd) {
56
+ try {
57
+ const ref = git("rev-parse --abbrev-ref origin/HEAD", cwd);
58
+ return ref.replace("origin/", "");
59
+ } catch {
60
+ try {
61
+ git("rev-parse --verify origin/main", cwd);
62
+ return "main";
63
+ } catch {
64
+ return "master";
65
+ }
66
+ }
67
+ }
68
+ function getCurrentBranch(cwd) {
69
+ return git("branch --show-current", cwd);
70
+ }
71
+ function fetchOrigin(cwd) {
72
+ git("fetch origin", cwd);
73
+ }
74
+ function checkoutNewBranch(name, base, cwd) {
75
+ git(`checkout -b ${name} origin/${base}`, cwd);
76
+ }
77
+ function stageAll(cwd) {
78
+ git("add -A", cwd);
79
+ try {
80
+ git("reset HEAD -- .yapout/", cwd);
81
+ } catch {
82
+ }
83
+ }
84
+ function commit(message, cwd) {
85
+ git(`commit -m "${message.replace(/"/g, '\\"')}"`, cwd);
86
+ return git("rev-parse HEAD", cwd);
87
+ }
88
+ function push(branch, cwd) {
89
+ git(`push -u origin ${branch}`, cwd);
90
+ }
91
+ function getDiffStats(base, head, cwd) {
92
+ try {
93
+ return git(`diff --stat origin/${base}...${head}`, cwd);
94
+ } catch {
95
+ return "(could not compute diff stats)";
96
+ }
97
+ }
98
+
99
+ // src/lib/config.ts
31
100
  import { parse as yamlParse } from "yaml";
32
101
  var YAPOUT_DIR = join(homedir(), ".yapout");
33
102
  function ensureYapoutDir() {
@@ -72,19 +141,46 @@ function writeProjectMappings(mappings) {
72
141
  writeFileSync(PROJECTS_PATH, JSON.stringify(mappings, null, 2));
73
142
  }
74
143
  function getProjectMapping(dir) {
144
+ const root = resolveRepoRoot(dir);
75
145
  const mappings = readProjectMappings();
76
- return mappings[dir] || null;
146
+ return mappings[root] || mappings[dir] || null;
77
147
  }
78
148
  function setProjectMapping(dir, mapping) {
79
149
  const mappings = readProjectMappings();
80
- mappings[dir] = mapping;
150
+ mappings[resolveRepoRoot(dir)] = mapping;
81
151
  writeProjectMappings(mappings);
82
152
  }
83
153
  function removeProjectMapping(dir) {
154
+ const root = resolveRepoRoot(dir);
84
155
  const mappings = readProjectMappings();
85
- delete mappings[dir];
156
+ delete mappings[root];
157
+ if (root !== dir) delete mappings[dir];
86
158
  writeProjectMappings(mappings);
87
159
  }
160
+ var DEVICE_PATH = join(YAPOUT_DIR, "device.json");
161
+ function readDeviceIdentity() {
162
+ if (!existsSync(DEVICE_PATH)) return null;
163
+ try {
164
+ return JSON.parse(readFileSync(DEVICE_PATH, "utf-8"));
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+ function writeDeviceIdentity(identity) {
170
+ ensureYapoutDir();
171
+ writeFileSync(DEVICE_PATH, JSON.stringify(identity, null, 2), { mode: 384 });
172
+ }
173
+ function getOrCreateDeviceIdentity(defaultName) {
174
+ const existing = readDeviceIdentity();
175
+ if (existing) return existing;
176
+ const identity = {
177
+ deviceId: typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `dev-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
178
+ name: defaultName,
179
+ createdAt: Date.now()
180
+ };
181
+ writeDeviceIdentity(identity);
182
+ return identity;
183
+ }
88
184
  var WATCH_DEFAULTS = {
89
185
  auto_enrich: true,
90
186
  auto_implement: true,
@@ -97,7 +193,7 @@ var CONFIG_DEFAULTS = {
97
193
  watch: { ...WATCH_DEFAULTS }
98
194
  };
99
195
  function readYapoutConfig(cwd) {
100
- const configPath = join(cwd, ".yapout", "config.yml");
196
+ const configPath = join(resolveRepoRoot(cwd), ".yapout", "config.yml");
101
197
  if (!existsSync(configPath)) return { ...CONFIG_DEFAULTS };
102
198
  try {
103
199
  const raw = yamlParse(readFileSync(configPath, "utf-8"));
@@ -179,10 +275,10 @@ function createConvexClient(token) {
179
275
  }
180
276
 
181
277
  // src/lib/protocol.ts
182
- import { execSync, spawnSync } from "child_process";
278
+ import { execSync as execSync2, spawnSync } from "child_process";
183
279
  import { platform, homedir as homedir2 } from "os";
184
280
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
185
- import { join as join2, dirname } from "path";
281
+ import { join as join2, dirname as dirname2 } from "path";
186
282
  function getYapoutBinPath() {
187
283
  const os = platform();
188
284
  try {
@@ -208,7 +304,7 @@ function registerWindows(yapoutPath) {
208
304
  `reg add "${key}\\shell\\open\\command" /ve /d "${handler}" /f`
209
305
  ];
210
306
  for (const cmd of commands) {
211
- execSync(cmd, { stdio: "pipe" });
307
+ execSync2(cmd, { stdio: "pipe" });
212
308
  }
213
309
  }
214
310
  function registerMacOS(_yapoutPath) {
@@ -361,11 +457,11 @@ on idle
361
457
  end idle`;
362
458
  writeFileSync2(tmpScript, scriptContent);
363
459
  try {
364
- execSync(`rm -rf "${appPath}"`, { stdio: "pipe" });
460
+ execSync2(`rm -rf "${appPath}"`, { stdio: "pipe" });
365
461
  } catch {
366
462
  }
367
- mkdirSync2(dirname(appPath), { recursive: true });
368
- execSync(`osacompile -s -o "${appPath}" "${tmpScript}"`, { stdio: "pipe" });
463
+ mkdirSync2(dirname2(appPath), { recursive: true });
464
+ execSync2(`osacompile -s -o "${appPath}" "${tmpScript}"`, { stdio: "pipe" });
369
465
  try {
370
466
  unlinkSync2(tmpScript);
371
467
  } catch {
@@ -381,12 +477,12 @@ end idle`;
381
477
  ];
382
478
  for (const cmd of plistCommands) {
383
479
  try {
384
- execSync(`/usr/libexec/PlistBuddy -c '${cmd}' "${plistPath}"`, { stdio: "pipe" });
480
+ execSync2(`/usr/libexec/PlistBuddy -c '${cmd}' "${plistPath}"`, { stdio: "pipe" });
385
481
  } catch {
386
482
  }
387
483
  }
388
484
  try {
389
- execSync(
485
+ execSync2(
390
486
  `/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -R "${appPath}"`,
391
487
  { stdio: "pipe" }
392
488
  );
@@ -406,7 +502,7 @@ MimeType=x-scheme-handler/yapout;
406
502
  `;
407
503
  writeFileSync2(join2(appsDir, "yapout-handler.desktop"), desktop);
408
504
  try {
409
- execSync(
505
+ execSync2(
410
506
  `xdg-mime default yapout-handler.desktop x-scheme-handler/yapout`,
411
507
  { stdio: "pipe" }
412
508
  );
@@ -426,7 +522,7 @@ function registerProtocolHandler() {
426
522
  }
427
523
 
428
524
  // src/commands/login.ts
429
- var CLI_VERSION = "0.5.2";
525
+ var CLI_VERSION = "0.8.0";
430
526
  function safeReturnTo(raw) {
431
527
  if (!raw) return null;
432
528
  try {
@@ -438,7 +534,7 @@ function safeReturnTo(raw) {
438
534
  }
439
535
  }
440
536
  function startCallbackServer() {
441
- return new Promise((resolve11) => {
537
+ return new Promise((resolve12) => {
442
538
  let resolveData;
443
539
  let rejectData;
444
540
  const dataPromise = new Promise((res, rej) => {
@@ -487,7 +583,7 @@ function startCallbackServer() {
487
583
  server.listen(0, () => {
488
584
  const address = server.address();
489
585
  const port = typeof address === "object" && address ? address.port : 0;
490
- resolve11({ port, data: dataPromise });
586
+ resolve12({ port, data: dataPromise });
491
587
  });
492
588
  setTimeout(() => {
493
589
  server.close();
@@ -558,7 +654,7 @@ var logoutCommand = new Command2("logout").description("Log out of yapout").acti
558
654
 
559
655
  // src/commands/link.ts
560
656
  import { Command as Command3 } from "commander";
561
- import { resolve, join as join3 } from "path";
657
+ import { resolve as resolve2, join as join3 } from "path";
562
658
  import {
563
659
  existsSync as existsSync2,
564
660
  mkdirSync as mkdirSync3,
@@ -566,6 +662,7 @@ import {
566
662
  writeFileSync as writeFileSync3,
567
663
  appendFileSync
568
664
  } from "fs";
665
+ import { hostname as hostname2 } from "os";
569
666
  import chalk4 from "chalk";
570
667
 
571
668
  // src/lib/auth.ts
@@ -592,9 +689,22 @@ import { select } from "@inquirer/prompts";
592
689
  async function pickProject(projects) {
593
690
  return await select({
594
691
  message: "Select a project to link to this directory:",
595
- choices: projects.map((p) => ({
596
- name: p.githubRepoFullName ? `${p.name} (${p.githubRepoFullName})` : p.name,
597
- value: p
692
+ choices: projects.map((p) => {
693
+ const repo = p.githubRepoFullName ? ` (${p.githubRepoFullName})` : "";
694
+ const orgLabel = p.org ? ` [${p.org.name}]` : "";
695
+ return {
696
+ name: `${p.name}${repo}${orgLabel}`,
697
+ value: p
698
+ };
699
+ })
700
+ });
701
+ }
702
+ async function pickOrg(orgs, message = "Which org does this project belong to?") {
703
+ return await select({
704
+ message,
705
+ choices: orgs.map((o) => ({
706
+ name: `${o.name} (${o.slug}, ${o.role})`,
707
+ value: o
598
708
  }))
599
709
  });
600
710
  }
@@ -626,7 +736,7 @@ branch_prefix: feat
626
736
  `;
627
737
  var linkCommand = new Command3("link").description("Link the current directory to a yapout project").action(async () => {
628
738
  const creds = requireAuth();
629
- const cwd = resolve(process.cwd());
739
+ const cwd = resolveRepoRoot(resolve2(process.cwd()));
630
740
  const client = createConvexClient(creds.token);
631
741
  let projects;
632
742
  try {
@@ -648,6 +758,25 @@ var linkCommand = new Command3("link").description("Link the current directory t
648
758
  process.exit(1);
649
759
  }
650
760
  const selected = await pickProject(projects);
761
+ const device = getOrCreateDeviceIdentity(hostname2());
762
+ try {
763
+ await client.mutation(anyApi.functions.devices.registerDevice, {
764
+ deviceId: device.deviceId,
765
+ name: device.name,
766
+ cliVersion: getCliVersion(),
767
+ machineHostname: hostname2()
768
+ });
769
+ await client.mutation(anyApi.functions.projectCheckouts.linkCheckout, {
770
+ projectId: selected.id,
771
+ deviceId: device.deviceId,
772
+ localPath: cwd
773
+ });
774
+ } catch (err) {
775
+ console.warn(
776
+ chalk4.yellow("Warning: failed to record this checkout \u2014 "),
777
+ err.message
778
+ );
779
+ }
651
780
  setProjectMapping(cwd, {
652
781
  projectId: selected.id,
653
782
  projectName: selected.name,
@@ -688,23 +817,50 @@ var linkCommand = new Command3("link").description("Link the current directory t
688
817
  };
689
818
  writeFileSync3(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
690
819
  const label = selected.githubRepoFullName ? `${selected.name} (${selected.githubRepoFullName})` : selected.name;
820
+ const orgSuffix = selected.org ? ` in ${selected.org.name}` : "";
691
821
  console.log(
692
- chalk4.green(`Linked to ${label}.`) + " Claude Code will discover yapout tools automatically."
822
+ chalk4.green(`Linked to ${label}${orgSuffix}.`) + " Claude Code will discover yapout tools automatically."
693
823
  );
694
824
  });
825
+ function getCliVersion() {
826
+ try {
827
+ const pkg = JSON.parse(
828
+ readFileSync2(join3(import.meta.dirname, "..", "package.json"), "utf-8")
829
+ );
830
+ return pkg.version ?? "unknown";
831
+ } catch {
832
+ return "unknown";
833
+ }
834
+ }
695
835
 
696
836
  // src/commands/unlink.ts
697
837
  import { Command as Command4 } from "commander";
698
- import { resolve as resolve2, join as join4 } from "path";
838
+ import { resolve as resolve3, join as join4 } from "path";
699
839
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4, rmSync } from "fs";
700
840
  import chalk5 from "chalk";
701
- var unlinkCommand = new Command4("unlink").description("Unlink the current directory from its yapout project").action(() => {
702
- const cwd = resolve2(process.cwd());
841
+ var unlinkCommand = new Command4("unlink").description("Unlink the current directory from its yapout project").action(async () => {
842
+ const cwd = resolve3(process.cwd());
703
843
  const mapping = getProjectMapping(cwd);
704
844
  if (!mapping) {
705
845
  console.error(chalk5.yellow("No project linked to this directory."));
706
846
  process.exit(1);
707
847
  }
848
+ const device = readDeviceIdentity();
849
+ const creds = readCredentials();
850
+ if (device && creds) {
851
+ try {
852
+ const client = createConvexClient(creds.token);
853
+ await client.mutation(anyApi.functions.projectCheckouts.unlinkCheckout, {
854
+ projectId: mapping.projectId,
855
+ deviceId: device.deviceId
856
+ });
857
+ } catch (err) {
858
+ console.warn(
859
+ chalk5.yellow("Warning: failed to update server \u2014 "),
860
+ err.message
861
+ );
862
+ }
863
+ }
708
864
  removeProjectMapping(cwd);
709
865
  const yapoutDir = join4(cwd, ".yapout");
710
866
  if (existsSync3(yapoutDir)) {
@@ -733,7 +889,7 @@ var unlinkCommand = new Command4("unlink").description("Unlink the current direc
733
889
 
734
890
  // src/commands/status.ts
735
891
  import { Command as Command5 } from "commander";
736
- import { resolve as resolve3 } from "path";
892
+ import { resolve as resolve4 } from "path";
737
893
  import chalk6 from "chalk";
738
894
  var statusCommand = new Command5("status").description("Show yapout status for this directory").action(() => {
739
895
  console.log(chalk6.bold("yapout status\n"));
@@ -755,7 +911,7 @@ var statusCommand = new Command5("status").description("Show yapout status for t
755
911
  ` Auth: ${chalk6.green(creds.email)} (expires in ${daysLeft} day${daysLeft === 1 ? "" : "s"})`
756
912
  );
757
913
  }
758
- const cwd = resolve3(process.cwd());
914
+ const cwd = resolve4(process.cwd());
759
915
  const mapping = getProjectMapping(cwd);
760
916
  if (!mapping) {
761
917
  console.log(
@@ -771,7 +927,7 @@ var statusCommand = new Command5("status").description("Show yapout status for t
771
927
 
772
928
  // src/commands/init.ts
773
929
  import { Command as Command6 } from "commander";
774
- import { resolve as resolve4, join as join5 } from "path";
930
+ import { resolve as resolve5, join as join5 } from "path";
775
931
  import {
776
932
  existsSync as existsSync4,
777
933
  mkdirSync as mkdirSync4,
@@ -779,66 +935,8 @@ import {
779
935
  readFileSync as readFileSync4,
780
936
  appendFileSync as appendFileSync2
781
937
  } from "fs";
938
+ import { hostname as hostname3 } from "os";
782
939
  import chalk7 from "chalk";
783
-
784
- // src/lib/git.ts
785
- import { execSync as execSync2 } from "child_process";
786
- function git(args, cwd) {
787
- return execSync2(`git ${args}`, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
788
- }
789
- function getRepoFullName(cwd) {
790
- const url = git("remote get-url origin", cwd);
791
- const sshMatch = url.match(/git@github\.com:(.+?)(?:\.git)?$/);
792
- if (sshMatch) return sshMatch[1];
793
- const httpsMatch = url.match(/github\.com\/(.+?)(?:\.git)?$/);
794
- if (httpsMatch) return httpsMatch[1];
795
- throw new Error(`Could not parse GitHub repo from remote URL: ${url}`);
796
- }
797
- function getDefaultBranch(cwd) {
798
- try {
799
- const ref = git("rev-parse --abbrev-ref origin/HEAD", cwd);
800
- return ref.replace("origin/", "");
801
- } catch {
802
- try {
803
- git("rev-parse --verify origin/main", cwd);
804
- return "main";
805
- } catch {
806
- return "master";
807
- }
808
- }
809
- }
810
- function getCurrentBranch(cwd) {
811
- return git("branch --show-current", cwd);
812
- }
813
- function fetchOrigin(cwd) {
814
- git("fetch origin", cwd);
815
- }
816
- function checkoutNewBranch(name, base, cwd) {
817
- git(`checkout -b ${name} origin/${base}`, cwd);
818
- }
819
- function stageAll(cwd) {
820
- git("add -A", cwd);
821
- try {
822
- git("reset HEAD -- .yapout/", cwd);
823
- } catch {
824
- }
825
- }
826
- function commit(message, cwd) {
827
- git(`commit -m "${message.replace(/"/g, '\\"')}"`, cwd);
828
- return git("rev-parse HEAD", cwd);
829
- }
830
- function push(branch, cwd) {
831
- git(`push -u origin ${branch}`, cwd);
832
- }
833
- function getDiffStats(base, head, cwd) {
834
- try {
835
- return git(`diff --stat origin/${base}...${head}`, cwd);
836
- } catch {
837
- return "(could not compute diff stats)";
838
- }
839
- }
840
-
841
- // src/commands/init.ts
842
940
  var CONFIG_YAML_CONTENT2 = `# yapout local configuration
843
941
  # See: https://docs.yapout.dev/cli/config
844
942
 
@@ -863,9 +961,9 @@ branch_prefix: feat
863
961
  # {{ticket.linearTicketId}}, {{ticket.id}}
864
962
  # commit_template: "{{ticket.type}}({{ticket.linearTicketId}}): {{ticket.title}}"
865
963
  `;
866
- var initCommand = new Command6("init").description("Create a yapout project from the current repo and link it").argument("[name]", "Project name (defaults to repo name)").action(async (name) => {
964
+ var initCommand = new Command6("init").description("Create a yapout project from the current repo and link it").argument("[name]", "Project name (defaults to repo name)").option("--org <slug>", "Org slug to create the project in (skips picker)").action(async (name, options) => {
867
965
  const creds = requireAuth();
868
- const cwd = resolve4(process.cwd());
966
+ const cwd = resolveRepoRoot(resolve5(process.cwd()));
869
967
  let repoFullName;
870
968
  let defaultBranch;
871
969
  try {
@@ -880,11 +978,82 @@ var initCommand = new Command6("init").description("Create a yapout project from
880
978
  }
881
979
  const projectName = name || repoFullName.split("/")[1] || "unnamed";
882
980
  const client = createConvexClient(creds.token);
981
+ let orgs;
982
+ try {
983
+ orgs = await client.query(
984
+ anyApi.functions.orgMembers.getMyOrgs,
985
+ {}
986
+ );
987
+ } catch (err) {
988
+ console.error(
989
+ chalk7.red("Failed to load your orgs."),
990
+ err.message
991
+ );
992
+ process.exit(1);
993
+ }
994
+ if (!orgs || orgs.length === 0) {
995
+ console.error(
996
+ chalk7.red(
997
+ "You aren't a member of any org. Sign in to the web app once to create your personal org, then re-run."
998
+ )
999
+ );
1000
+ process.exit(1);
1001
+ }
1002
+ let chosenOrgId;
1003
+ let chosenOrgName;
1004
+ if (options?.org) {
1005
+ const match = orgs.find((o) => o.org.slug === options.org);
1006
+ if (!match) {
1007
+ console.error(
1008
+ chalk7.red(`Org "${options.org}" not found among your memberships.`)
1009
+ );
1010
+ console.error(
1011
+ chalk7.dim(
1012
+ "Available: " + orgs.map((o) => o.org.slug).join(", ")
1013
+ )
1014
+ );
1015
+ process.exit(1);
1016
+ }
1017
+ chosenOrgId = match.org._id;
1018
+ chosenOrgName = match.org.name;
1019
+ } else if (orgs.length === 1) {
1020
+ chosenOrgId = orgs[0].org._id;
1021
+ chosenOrgName = orgs[0].org.name;
1022
+ console.log(
1023
+ chalk7.dim(`Creating in `) + chalk7.cyan(chosenOrgName)
1024
+ );
1025
+ } else {
1026
+ const picked = await pickOrg(
1027
+ orgs.map((o) => ({
1028
+ id: o.org._id,
1029
+ name: o.org.name,
1030
+ slug: o.org.slug,
1031
+ role: o.role
1032
+ }))
1033
+ );
1034
+ chosenOrgId = picked.id;
1035
+ chosenOrgName = picked.name;
1036
+ }
1037
+ const device = getOrCreateDeviceIdentity(hostname3());
1038
+ try {
1039
+ await client.mutation(anyApi.functions.devices.registerDevice, {
1040
+ deviceId: device.deviceId,
1041
+ name: device.name,
1042
+ cliVersion: getCliVersion2(),
1043
+ machineHostname: hostname3()
1044
+ });
1045
+ } catch (err) {
1046
+ console.warn(
1047
+ chalk7.yellow("Warning: device registration failed \u2014 "),
1048
+ err.message
1049
+ );
1050
+ }
883
1051
  let result;
884
1052
  try {
885
1053
  result = await client.mutation(
886
1054
  anyApi.functions.projects.createProjectFromCli,
887
1055
  {
1056
+ orgId: chosenOrgId,
888
1057
  name: projectName,
889
1058
  githubRepoFullName: repoFullName,
890
1059
  githubDefaultBranch: defaultBranch
@@ -897,6 +1066,18 @@ var initCommand = new Command6("init").description("Create a yapout project from
897
1066
  );
898
1067
  process.exit(1);
899
1068
  }
1069
+ try {
1070
+ await client.mutation(anyApi.functions.projectCheckouts.linkCheckout, {
1071
+ projectId: result.projectId,
1072
+ deviceId: device.deviceId,
1073
+ localPath: cwd
1074
+ });
1075
+ } catch (err) {
1076
+ console.warn(
1077
+ chalk7.yellow("Warning: failed to record project checkout \u2014 "),
1078
+ err.message
1079
+ );
1080
+ }
900
1081
  setProjectMapping(cwd, {
901
1082
  projectId: result.projectId,
902
1083
  projectName: result.projectName,
@@ -933,12 +1114,24 @@ var initCommand = new Command6("init").description("Create a yapout project from
933
1114
  };
934
1115
  writeFileSync5(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
935
1116
  console.log(
936
- chalk7.green(`Created project "${result.projectName}"`) + chalk7.dim(` (${repoFullName}, branch: ${defaultBranch})`)
1117
+ chalk7.green(`Created project "${result.projectName}"`) + chalk7.dim(
1118
+ ` in ${chosenOrgName} (${repoFullName}, branch: ${defaultBranch})`
1119
+ )
937
1120
  );
938
1121
  console.log(
939
1122
  chalk7.dim("Run ") + chalk7.cyan("yapout_compact") + chalk7.dim(" in Claude Code to generate project context.")
940
1123
  );
941
1124
  });
1125
+ function getCliVersion2() {
1126
+ try {
1127
+ const pkg = JSON.parse(
1128
+ readFileSync4(join5(import.meta.dirname, "..", "package.json"), "utf-8")
1129
+ );
1130
+ return pkg.version ?? "unknown";
1131
+ } catch {
1132
+ return "unknown";
1133
+ }
1134
+ }
942
1135
 
943
1136
  // src/commands/mcp-server.ts
944
1137
  import { Command as Command7 } from "commander";
@@ -963,8 +1156,9 @@ function registerInitTool(server, ctx) {
963
1156
  linearTeamId: z.string().optional().describe("Linear team ID to associate")
964
1157
  },
965
1158
  async (args) => {
966
- const repoFullName = getRepoFullName(ctx.cwd);
967
- const defaultBranch = getDefaultBranch(ctx.cwd);
1159
+ const repoRoot = resolveRepoRoot(ctx.cwd);
1160
+ const repoFullName = getRepoFullName(repoRoot);
1161
+ const defaultBranch = getDefaultBranch(repoRoot);
968
1162
  const projectName = args.name || repoFullName.split("/")[1] || "unnamed";
969
1163
  const result = await ctx.client.mutation(
970
1164
  anyApi3.functions.projects.createProjectFromCli,
@@ -977,12 +1171,12 @@ function registerInitTool(server, ctx) {
977
1171
  );
978
1172
  ctx.projectId = result.projectId;
979
1173
  ctx.projectName = result.projectName;
980
- setProjectMapping(ctx.cwd, {
1174
+ setProjectMapping(repoRoot, {
981
1175
  projectId: result.projectId,
982
1176
  projectName: result.projectName,
983
1177
  linkedAt: Date.now()
984
1178
  });
985
- const yapoutDir = join6(ctx.cwd, ".yapout");
1179
+ const yapoutDir = join6(repoRoot, ".yapout");
986
1180
  if (!existsSync5(yapoutDir)) mkdirSync5(yapoutDir, { recursive: true });
987
1181
  const configPath = join6(yapoutDir, "config.yml");
988
1182
  if (!existsSync5(configPath)) {
@@ -2125,7 +2319,7 @@ function truncate(text) {
2125
2319
  ` + lines.slice(-MAX_OUTPUT_LINES).join("\n");
2126
2320
  }
2127
2321
  function runCommand(command, cwd) {
2128
- return new Promise((resolve11) => {
2322
+ return new Promise((resolve12) => {
2129
2323
  const start = Date.now();
2130
2324
  const child = exec(command, {
2131
2325
  cwd,
@@ -2133,7 +2327,7 @@ function runCommand(command, cwd) {
2133
2327
  maxBuffer: 10 * 1024 * 1024
2134
2328
  // 10MB
2135
2329
  }, (error, stdout, stderr) => {
2136
- resolve11({
2330
+ resolve12({
2137
2331
  exitCode: error?.code ?? (error ? 1 : 0),
2138
2332
  stdout: truncate(stdout),
2139
2333
  stderr: truncate(stderr),
@@ -2141,7 +2335,7 @@ function runCommand(command, cwd) {
2141
2335
  });
2142
2336
  });
2143
2337
  child.on("error", () => {
2144
- resolve11({
2338
+ resolve12({
2145
2339
  exitCode: 1,
2146
2340
  stdout: "",
2147
2341
  stderr: `Command timed out after ${COMMAND_TIMEOUT_MS / 1e3}s`,
@@ -2503,6 +2697,7 @@ The finding transitions: enriching \u2192 enriched \u2192 ready.`,
2503
2697
  isOversized: z11.boolean().optional().describe("Set to true if this finding is too large for a single PR"),
2504
2698
  suggestedSplit: z11.array(z11.string()).optional().describe("If oversized: suggested sub-finding titles for breaking it down"),
2505
2699
  nature: z11.enum(["implementable", "operational", "spike"]).optional().describe("Override the finding's nature if enrichment reveals it should be reclassified"),
2700
+ cloudSafe: z11.boolean().optional().describe("Set to true ONLY if this is a small mechanical change a cloud agent can ship without sandbox testing \u2014 text/copy edits, classname tweaks, single-named-constant changes, \u226430 lines, single file, no logic/type/dependency changes. Default false."),
2506
2701
  sessionId: z11.string().optional().describe("Bulk enrichment session ID (from yapout_start_enrichment). Updates session stats.")
2507
2702
  },
2508
2703
  async (args) => {
@@ -2518,7 +2713,8 @@ The finding transitions: enriching \u2192 enriched \u2192 ready.`,
2518
2713
  clarifications: args.clarifications,
2519
2714
  isOversized: args.isOversized,
2520
2715
  suggestedSplit: args.suggestedSplit,
2521
- nature: args.nature
2716
+ nature: args.nature,
2717
+ cloudSafe: args.cloudSafe
2522
2718
  }
2523
2719
  );
2524
2720
  await ctx.client.action(
@@ -3183,7 +3379,7 @@ and issue counts so you can ask the user for confirmation before creating a new
3183
3379
  }
3184
3380
  const projects = await ctx.client.action(
3185
3381
  anyApi3.functions.linearProjectsMutations.fetchProjectsDetailed,
3186
- { teamId: project.linearTeamId }
3382
+ { projectId: ctx.projectId, teamId: project.linearTeamId }
3187
3383
  );
3188
3384
  return {
3189
3385
  content: [
@@ -3494,6 +3690,7 @@ Call yapout_sync_bundle_to_linear afterwards to create the Linear project.`,
3494
3690
  enrichedDescription: z21.string().describe("Bundle-level description \u2014 the cohesive story of what this bundle delivers"),
3495
3691
  acceptanceCriteria: z21.array(z21.string()).describe("Bundle-level acceptance criteria"),
3496
3692
  implementationBrief: z21.string().describe("Bundle-level implementation brief \u2014 overall approach, architecture decisions, key files"),
3693
+ cloudSafe: z21.boolean().optional().describe("Set true ONLY if the entire bundle is small mechanical work (\u226430 lines total, single-file ideally, text/className/constant edits). Default false. If unsure, false."),
3497
3694
  findings: z21.array(z21.object({
3498
3695
  findingId: z21.string().describe("Finding ID"),
3499
3696
  title: z21.string().describe("Refined finding title"),
@@ -3512,6 +3709,7 @@ Call yapout_sync_bundle_to_linear afterwards to create the Linear project.`,
3512
3709
  enrichedDescription: args.enrichedDescription,
3513
3710
  acceptanceCriteria: args.acceptanceCriteria,
3514
3711
  implementationBrief: args.implementationBrief,
3712
+ cloudSafe: args.cloudSafe,
3515
3713
  findings: args.findings.map((f) => ({
3516
3714
  findingId: f.findingId,
3517
3715
  title: f.title,
@@ -3543,6 +3741,113 @@ Call yapout_sync_bundle_to_linear afterwards to create the Linear project.`,
3543
3741
  );
3544
3742
  }
3545
3743
 
3744
+ // src/mcp/tools/block-enrichment.ts
3745
+ import { z as z22 } from "zod";
3746
+ function registerBlockEnrichmentTool(server, ctx) {
3747
+ server.tool(
3748
+ "yapout_block_enrichment",
3749
+ `Refuse to enrich a finding because the user has not provided enough specifics for an autonomous agent to complete the work end-to-end.
3750
+
3751
+ Use this when the finding lacks any of: target file/component, intended behavior, scope boundaries, or success criteria. Do NOT produce a half-baked enrichment "for review" \u2014 block instead. The bar for "enriched" is "another agent can ship this with zero further input."
3752
+
3753
+ The finding must currently be in "enriching" status (claimed via yapout_get_unenriched_finding).
3754
+
3755
+ Transitions: enriching \u2192 needs_input. The user sees the blockerReason and questions in the UI, adds context, and resubmits.`,
3756
+ {
3757
+ findingId: z22.string().describe("The finding ID to block (currently in 'enriching')"),
3758
+ blockerReason: z22.string().describe("One sentence: why this finding cannot be enriched as written. State the missing piece concretely."),
3759
+ blockerQuestions: z22.array(z22.string()).min(1).describe("2-5 specific questions the user must answer before enrichment can succeed. Each must be answerable in a sentence or two.")
3760
+ },
3761
+ async (args) => {
3762
+ try {
3763
+ await ctx.client.mutation(
3764
+ anyApi3.functions.localPipeline.blockLocalEnrichment,
3765
+ {
3766
+ findingId: args.findingId,
3767
+ blockerReason: args.blockerReason,
3768
+ blockerQuestions: args.blockerQuestions
3769
+ }
3770
+ );
3771
+ return {
3772
+ content: [
3773
+ {
3774
+ type: "text",
3775
+ text: JSON.stringify(
3776
+ {
3777
+ findingId: args.findingId,
3778
+ status: "needs_input",
3779
+ message: "Enrichment blocked. The user will see your questions and resubmit with more context."
3780
+ },
3781
+ null,
3782
+ 2
3783
+ )
3784
+ }
3785
+ ]
3786
+ };
3787
+ } catch (err) {
3788
+ return {
3789
+ content: [
3790
+ {
3791
+ type: "text",
3792
+ text: `Error blocking enrichment: ${err.message}`
3793
+ }
3794
+ ],
3795
+ isError: true
3796
+ };
3797
+ }
3798
+ }
3799
+ );
3800
+ server.tool(
3801
+ "yapout_block_bundle_enrichment",
3802
+ `Refuse to enrich a bundle because the user has not provided enough specifics for an autonomous agent to ship it end-to-end.
3803
+
3804
+ Same semantics as yapout_block_enrichment but for an entire bundle. Bundle status transitions enriching \u2192 needs_input; child findings revert to draft.`,
3805
+ {
3806
+ bundleId: z22.string().describe("The bundle ID to block (currently in 'enriching')"),
3807
+ blockerReason: z22.string().describe("One sentence: why this bundle cannot be enriched."),
3808
+ blockerQuestions: z22.array(z22.string()).min(1).describe("2-5 specific questions the user must answer.")
3809
+ },
3810
+ async (args) => {
3811
+ try {
3812
+ await ctx.client.mutation(
3813
+ anyApi3.functions.bundles.blockBundleEnrichment,
3814
+ {
3815
+ bundleId: args.bundleId,
3816
+ blockerReason: args.blockerReason,
3817
+ blockerQuestions: args.blockerQuestions
3818
+ }
3819
+ );
3820
+ return {
3821
+ content: [
3822
+ {
3823
+ type: "text",
3824
+ text: JSON.stringify(
3825
+ {
3826
+ bundleId: args.bundleId,
3827
+ status: "needs_input",
3828
+ message: "Bundle enrichment blocked. The user will see your questions and resubmit."
3829
+ },
3830
+ null,
3831
+ 2
3832
+ )
3833
+ }
3834
+ ]
3835
+ };
3836
+ } catch (err) {
3837
+ return {
3838
+ content: [
3839
+ {
3840
+ type: "text",
3841
+ text: `Error blocking bundle enrichment: ${err.message}`
3842
+ }
3843
+ ],
3844
+ isError: true
3845
+ };
3846
+ }
3847
+ }
3848
+ );
3849
+ }
3850
+
3546
3851
  // src/mcp/server.ts
3547
3852
  async function startMcpServer() {
3548
3853
  const cwd = process.cwd();
@@ -3573,7 +3878,7 @@ async function startMcpServer() {
3573
3878
  };
3574
3879
  const server = new McpServer({
3575
3880
  name: "yapout",
3576
- version: "0.5.2"
3881
+ version: "0.8.0"
3577
3882
  });
3578
3883
  registerInitTool(server, ctx);
3579
3884
  registerCompactTool(server, ctx);
@@ -3599,6 +3904,7 @@ async function startMcpServer() {
3599
3904
  registerEnrichNextTool(server, ctx);
3600
3905
  registerEnrichBundleTool(server, ctx);
3601
3906
  registerSaveBundleEnrichmentTool(server, ctx);
3907
+ registerBlockEnrichmentTool(server, ctx);
3602
3908
  const transport = new StdioServerTransport();
3603
3909
  await server.connect(transport);
3604
3910
  }
@@ -3611,9 +3917,9 @@ var mcpServerCommand = new Command7("mcp-server").description("Start the MCP ser
3611
3917
  // src/commands/worktrees.ts
3612
3918
  import { Command as Command8 } from "commander";
3613
3919
  import chalk8 from "chalk";
3614
- import { resolve as resolve5 } from "path";
3920
+ import { resolve as resolve6 } from "path";
3615
3921
  var worktreesCommand = new Command8("worktrees").description("List active yapout worktrees").action(() => {
3616
- const cwd = resolve5(process.cwd());
3922
+ const cwd = resolve6(process.cwd());
3617
3923
  const worktrees = listWorktrees(cwd);
3618
3924
  if (worktrees.length === 0) {
3619
3925
  console.log(chalk8.dim("No active yapout worktrees."));
@@ -3631,10 +3937,10 @@ var worktreesCommand = new Command8("worktrees").description("List active yapout
3631
3937
  // src/commands/clean.ts
3632
3938
  import { Command as Command9 } from "commander";
3633
3939
  import chalk9 from "chalk";
3634
- import { resolve as resolve6 } from "path";
3940
+ import { resolve as resolve7 } from "path";
3635
3941
  var cleanCommand = new Command9("clean").description("Remove worktrees for completed or failed tickets").action(async () => {
3636
3942
  const creds = requireAuth();
3637
- const cwd = resolve6(process.cwd());
3943
+ const cwd = resolve7(process.cwd());
3638
3944
  const worktrees = listWorktrees(cwd);
3639
3945
  if (worktrees.length === 0) {
3640
3946
  console.log(chalk9.dim("No worktrees to clean."));
@@ -3674,7 +3980,7 @@ var cleanCommand = new Command9("clean").description("Remove worktrees for compl
3674
3980
 
3675
3981
  // src/commands/watch.ts
3676
3982
  import { Command as Command10 } from "commander";
3677
- import { resolve as resolve7 } from "path";
3983
+ import { resolve as resolve8 } from "path";
3678
3984
  import {
3679
3985
  readFileSync as readFileSync6,
3680
3986
  writeFileSync as writeFileSync9,
@@ -3909,10 +4215,10 @@ var Spawner = class {
3909
4215
  if (this.agents.size === 0) return;
3910
4216
  const timeout = 10 * 60 * 1e3;
3911
4217
  const start = Date.now();
3912
- await new Promise((resolve11) => {
4218
+ await new Promise((resolve12) => {
3913
4219
  const check = () => {
3914
4220
  if (this.agents.size === 0 || Date.now() - start > timeout) {
3915
- resolve11();
4221
+ resolve12();
3916
4222
  return;
3917
4223
  }
3918
4224
  setTimeout(check, 2e3);
@@ -4322,7 +4628,7 @@ var watchCommand = new Command10("watch").description("Watch for work and spawn
4322
4628
  return;
4323
4629
  }
4324
4630
  const creds = requireAuth();
4325
- const cwd = resolve7(process.cwd());
4631
+ const cwd = resolve8(process.cwd());
4326
4632
  const mapping = getProjectMapping(cwd);
4327
4633
  if (!mapping) {
4328
4634
  console.error(
@@ -4339,7 +4645,7 @@ var watchCommand = new Command10("watch").description("Watch for work and spawn
4339
4645
  chalk11.green("Watcher started in background") + chalk11.dim(` (PID ${process.pid}, log: ${LOG_FILE})`)
4340
4646
  );
4341
4647
  }
4342
- console.log(chalk11.bold(`yapout watch v${"0.5.2"}`));
4648
+ console.log(chalk11.bold(`yapout watch v${"0.8.0"}`));
4343
4649
  console.log(
4344
4650
  `Project: ${chalk11.green(mapping.projectName)} (${mapping.projectId})`
4345
4651
  );
@@ -4394,11 +4700,11 @@ function isProcessRunning(pid) {
4394
4700
 
4395
4701
  // src/commands/queue.ts
4396
4702
  import { Command as Command11 } from "commander";
4397
- import { resolve as resolve8 } from "path";
4703
+ import { resolve as resolve9 } from "path";
4398
4704
  import chalk12 from "chalk";
4399
4705
  var queueCommand = new Command11("queue").description("Show pipeline state \u2014 what's ready, blocked, and pending").action(async () => {
4400
4706
  const creds = requireAuth();
4401
- const cwd = resolve8(process.cwd());
4707
+ const cwd = resolve9(process.cwd());
4402
4708
  const mapping = getProjectMapping(cwd);
4403
4709
  if (!mapping) {
4404
4710
  console.error(
@@ -4491,13 +4797,13 @@ function formatAgo(ms) {
4491
4797
 
4492
4798
  // src/commands/next.ts
4493
4799
  import { Command as Command12 } from "commander";
4494
- import { resolve as resolve9 } from "path";
4800
+ import { resolve as resolve10 } from "path";
4495
4801
  import { writeFileSync as writeFileSync10 } from "fs";
4496
4802
  import { join as join12 } from "path";
4497
4803
  import chalk13 from "chalk";
4498
4804
  var nextCommand = new Command12("next").description("Claim the highest priority ticket and set up for implementation").option("--worktree", "Create a git worktree instead of checking out a branch").action(async (opts) => {
4499
4805
  const creds = requireAuth();
4500
- const cwd = resolve9(process.cwd());
4806
+ const cwd = resolve10(process.cwd());
4501
4807
  const mapping = getProjectMapping(cwd);
4502
4808
  if (!mapping) {
4503
4809
  console.error(
@@ -4596,11 +4902,11 @@ function formatBrief(ref, ticket, brief) {
4596
4902
 
4597
4903
  // src/commands/recap.ts
4598
4904
  import { Command as Command13 } from "commander";
4599
- import { resolve as resolve10 } from "path";
4905
+ import { resolve as resolve11 } from "path";
4600
4906
  import chalk14 from "chalk";
4601
4907
  var recapCommand = new Command13("recap").description("Show a summary of recent yapout activity").option("--week", "Show full week summary (default: today)").action(async (opts) => {
4602
4908
  const creds = requireAuth();
4603
- const cwd = resolve10(process.cwd());
4909
+ const cwd = resolve11(process.cwd());
4604
4910
  const mapping = getProjectMapping(cwd);
4605
4911
  if (!mapping) {
4606
4912
  console.error(
@@ -4892,7 +5198,7 @@ var handleUriCommand = new Command15("handle-uri").description("Handle a yapout:
4892
5198
 
4893
5199
  // src/index.ts
4894
5200
  var program = new Command16();
4895
- program.name("yapout").description("yapout \u2014 from meeting transcript to merged PR").version("0.5.2");
5201
+ program.name("yapout").description("yapout \u2014 from meeting transcript to merged PR").version("0.8.0");
4896
5202
  program.addCommand(loginCommand);
4897
5203
  program.addCommand(logoutCommand);
4898
5204
  program.addCommand(initCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yapout",
3
- "version": "0.5.2",
3
+ "version": "0.8.0",
4
4
  "description": "yapout CLI — link local repos, authenticate, and manage projects",
5
5
  "type": "module",
6
6
  "bin": {