@jentrix/cli 0.5.15 → 0.5.16

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "jentrix",
3
3
  "description": "Connect this Claude Code session to a Jentrix project: /jentrix-connect binds the current session (trusted lifecycle hooks supply the session id), /jentrix-align runs the MVP alignment wizard, /jentrix-status and /jentrix-end wrap the same session operations. Business logic lives in the Jentrix CLI — removing this plugin leaves `jentrix session attach` as the universal fallback.",
4
- "version": "0.5.1"
4
+ "version": "0.5.2"
5
5
  }
@@ -21,6 +21,15 @@ and do NOT answer any question yourself.
21
21
  3. If `currentAlignment` is non-null: show it to the operator and ask —
22
22
  using the native choice UI (AskUserQuestion) — whether to keep it or
23
23
  re-align. Keep → relay the current snapshot and stop.
24
+ **Fast path.** If the payload's only `ask` question is `confirmation`
25
+ (project / work item / owner were pre-filled from this checkout's previous
26
+ alignment and the confirmation prompt says so — "continuing this
27
+ checkout's previous alignment …"), present that prompt VERBATIM through
28
+ the choice UI. Confirm ⇒ submit with the answered values exactly as the
29
+ payload shows them (`--project <id> --task <id>|--session-level --owner
30
+ <id>`). Re-answer ⇒ re-run `jentrix align --questions --json --fresh` and
31
+ continue with the full flow below, passing `--fresh` on the final submit
32
+ too.
24
33
  4. For each question whose `status` is `"ask"`, present its `choices`
25
34
  verbatim through the native choice UI (AskUserQuestion), preserving the
26
35
  labels. If the question carries a `notice`, relay it to the operator
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jentrix",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Connect Codex tasks to Jentrix projects with trusted lifecycle hooks, alignment, checkpoints, and honest session capture.",
5
5
  "author": {
6
6
  "name": "Jentrix",
@@ -10,6 +10,13 @@ description: Align the current Codex task to a Jentrix workspace, project, work
10
10
  2. Relay every failed or warning precondition before asking anything.
11
11
  3. If `currentAlignment` exists, ask whether to keep it or re-align. Keep means
12
12
  show the current snapshot and stop.
13
+ Fast path: if the payload's only `ask` question is `confirmation` (project /
14
+ work item / owner were pre-filled from this checkout's previous alignment and
15
+ the confirmation prompt says so), present that prompt verbatim. Confirm ⇒
16
+ submit with the answered values exactly as the payload shows them
17
+ (`--project <id> --task <id>|--session-level --owner <id>`). Re-answer ⇒
18
+ re-run `jentrix align --provider codex --questions --json --fresh` and
19
+ continue with the full flow below, passing `--fresh` on the final submit too.
13
20
  4. Present each `status:"ask"` question and its choices verbatim using Codex's
14
21
  structured input UI when available, otherwise ask in chat. Never answer for the
15
22
  operator. Preserve notices and the `Other…` free-text escape.
package/dist/main.js CHANGED
@@ -10334,10 +10334,11 @@ import {
10334
10334
  mkdirSync as mkdirSync4,
10335
10335
  openSync as openSync3,
10336
10336
  readFileSync as readFileSync7,
10337
- statSync as statSync3
10337
+ statSync as statSync3,
10338
+ writeFileSync as writeFileSync5
10338
10339
  } from "node:fs";
10339
10340
  import { homedir as homedir2 } from "node:os";
10340
- import { basename as basename2, delimiter as delimiter2, dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
10341
+ import { basename as basename2, delimiter as delimiter2, dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
10341
10342
  import { fileURLToPath } from "node:url";
10342
10343
 
10343
10344
  // node_modules/.pnpm/commander@14.0.3/node_modules/commander/esm.mjs
@@ -20965,7 +20966,7 @@ function refreshAccessToken(input) {
20965
20966
 
20966
20967
  // src/client.ts
20967
20968
  var CLI_NAME = "stacks-cli";
20968
- var CLI_VERSION = "0.5.15";
20969
+ var CLI_VERSION = "0.5.16";
20969
20970
  var DEAD_TOKEN_MESSAGE = "authentication failed (HTTP 401): the token is dead or expired \u2014 mint a new PAT at /account/tokens on your Jentrix server and update STACKS_TOKEN (or --token / the config file).";
20970
20971
  var RELOGIN_MESSAGE = "OAuth session expired and could not be refreshed \u2014 run `jentrix login` to sign in again.";
20971
20972
  function originOf2(url2) {
@@ -21316,6 +21317,15 @@ var PROJECT_NAME_PROMPT = "Name for the new project \u2014 your own words, exact
21316
21317
  function buildAlignDisclosures(catalog) {
21317
21318
  const disclosures = [];
21318
21319
  const repo = catalog.detection.repoOwnerName;
21320
+ const previous = catalog.previous;
21321
+ if (previous && previous.usedFor.length > 0) {
21322
+ const parts = previous.usedFor.map(
21323
+ (kind) => kind === "project" ? `project ${previous.projectName}` : kind === "work_item" ? `task ${previous.taskKey ?? "session-level"}` : `owner ${previous.ownerLabel}`
21324
+ );
21325
+ disclosures.push(
21326
+ `continuing this checkout's previous alignment \u2014 ${parts.join(" \xB7 ")} (choose Re-answer, or run with --fresh, to change)`
21327
+ );
21328
+ }
21319
21329
  if (catalog.chosenProjectId === "__pending__") {
21320
21330
  const name = catalog.newProjectName ?? "the new project";
21321
21331
  disclosures.push(
@@ -21420,7 +21430,10 @@ function buildAlignQuestions(catalog) {
21420
21430
  ...new Map(
21421
21431
  [
21422
21432
  ...catalog.candidates.map((candidate) => candidate.workspace),
21423
- ...catalog.pinnedWorkspace ? [catalog.pinnedWorkspace] : []
21433
+ ...catalog.pinnedWorkspace ? [catalog.pinnedWorkspace] : [],
21434
+ // D4: a repo that matches nothing offers the caller's own workspaces
21435
+ // instead of a lone "Other…" — the same skip/ask rules apply to them.
21436
+ ...catalog.candidates.length === 0 ? catalog.workspaces ?? [] : []
21424
21437
  ].map((ws) => [ws.id, ws])
21425
21438
  ).values()
21426
21439
  ];
@@ -21648,6 +21661,14 @@ function buildAlignQuestions(catalog) {
21648
21661
  };
21649
21662
  } else if (project.status !== "answered" || catalog.members === null) {
21650
21663
  owner = { id: "owner", prompt: "Owner", status: "pending", choices: [] };
21664
+ } else if (catalog.members.length === 1) {
21665
+ owner = {
21666
+ id: "owner",
21667
+ prompt: "Owner",
21668
+ status: "skipped",
21669
+ answer: catalog.members[0].id,
21670
+ choices: []
21671
+ };
21651
21672
  } else {
21652
21673
  owner = {
21653
21674
  id: "owner",
@@ -21666,7 +21687,8 @@ function buildAlignQuestions(catalog) {
21666
21687
  };
21667
21688
  }
21668
21689
  const targetKnown = project.status === "answered" && (projectName.status === "answered" || projectName.status === "skipped");
21669
- const ready = targetKnown && workItem.status === "answered" && (taskTitle.status === "answered" || taskTitle.status === "skipped") && owner.status === "answered";
21690
+ const ready = targetKnown && workItem.status === "answered" && (taskTitle.status === "answered" || taskTitle.status === "skipped") && // D5: a sole-member owner is resolved by detection, like the workspace.
21691
+ (owner.status === "answered" || owner.status === "skipped");
21670
21692
  const disclosures = targetKnown ? buildAlignDisclosures(catalog) : [];
21671
21693
  const confirmation = {
21672
21694
  id: "confirmation",
@@ -23682,6 +23704,78 @@ async function openTasksForProject(caller, boardIds) {
23682
23704
  tasks.sort((a, b) => b.number - a.number);
23683
23705
  return tasks.slice(0, MAX_WORK_ITEMS).map(({ id, key, title }) => ({ id, key, title }));
23684
23706
  }
23707
+ async function workspaceProjects(caller, workspaceId) {
23708
+ const listed = await callStructured(caller, "list_projects", { workspaceId });
23709
+ return (listed.projects ?? []).filter((project) => project.archivedAt === null).map(({ id, name, slug }) => ({ id, name, slug }));
23710
+ }
23711
+ async function workspaceMembers(caller, workspaceId) {
23712
+ const result = await callStructured(caller, "list_members", { workspaceId });
23713
+ return (result.members ?? []).map((member) => ({
23714
+ id: member.userId ?? member.id ?? "",
23715
+ name: member.name,
23716
+ email: member.email
23717
+ }));
23718
+ }
23719
+ async function loadProjectContext(caller, catalog, projectId) {
23720
+ try {
23721
+ const project = await projectBoards(caller, projectId);
23722
+ catalog.repoLinked = repoLinkedOf(catalog, projectId, project.repoOwners);
23723
+ catalog.projectHasBoard = project.boardIds.length > 0;
23724
+ catalog.openTasks = await openTasksForProject(caller, project.boardIds);
23725
+ catalog.members = await workspaceMembers(caller, project.workspaceId);
23726
+ return true;
23727
+ } catch {
23728
+ return false;
23729
+ }
23730
+ }
23731
+ async function previousAlignmentOf(caller, newest, prefetched = null) {
23732
+ try {
23733
+ const session = prefetched ?? await callStructured(caller, "get_agent_session", {
23734
+ sessionId: newest.sessionId
23735
+ });
23736
+ const a = session.alignment;
23737
+ if (!a?.project?.id || !a.owner?.id) return null;
23738
+ return {
23739
+ sessionId: newest.sessionId,
23740
+ workspaceId: a.workspace?.id ?? newest.workspaceId,
23741
+ project: { id: a.project.id, name: a.project.name ?? a.project.id },
23742
+ task: a.task?.id ? { id: a.task.id, key: a.task.key ?? a.task.id } : null,
23743
+ owner: {
23744
+ id: a.owner.id,
23745
+ label: a.owner.name ?? a.owner.email ?? a.owner.id
23746
+ }
23747
+ };
23748
+ } catch {
23749
+ return null;
23750
+ }
23751
+ }
23752
+ function prefillFromPrevious(previous, catalog, projectPrefilled) {
23753
+ const sameProject = projectPrefilled || catalog.chosenProjectId === previous.project.id;
23754
+ if (!sameProject) return null;
23755
+ const usedFor = [];
23756
+ if (projectPrefilled) usedFor.push("project");
23757
+ if (catalog.chosenWorkItem === null && catalog.openTasks !== null) {
23758
+ if (previous.task === null) {
23759
+ catalog.chosenWorkItem = "__none__";
23760
+ usedFor.push("work_item");
23761
+ } else if (catalog.openTasks.some((t) => t.id === previous.task.id)) {
23762
+ catalog.chosenWorkItem = previous.task.id;
23763
+ usedFor.push("work_item");
23764
+ }
23765
+ }
23766
+ if (catalog.chosenOwnerId === null && catalog.members?.some((m) => m.id === previous.owner.id)) {
23767
+ catalog.chosenOwnerId = previous.owner.id;
23768
+ usedFor.push("owner");
23769
+ }
23770
+ if (usedFor.length === 0) return null;
23771
+ return {
23772
+ sessionId: previous.sessionId,
23773
+ projectName: previous.project.name,
23774
+ taskKey: previous.task?.key ?? null,
23775
+ ownerLabel: previous.owner.label,
23776
+ usedFor
23777
+ };
23778
+ }
23685
23779
  async function resolveCandidates(caller, repoOwnerName) {
23686
23780
  try {
23687
23781
  const result = await callStructured(caller, "resolve_projects_for_repo", {
@@ -23847,8 +23941,8 @@ async function buildCatalog(caller, flags, detection) {
23847
23941
  const context = await callStructured(caller, "get_token_context", {});
23848
23942
  let pinnedWorkspace = null;
23849
23943
  if (context.workspacePinned === true && typeof context.workspaceId === "string") {
23850
- const workspaces = await callStructured(caller, "list_workspaces", {});
23851
- const match = (workspaces.workspaces ?? []).find((ws) => ws.id === context.workspaceId);
23944
+ const workspaces2 = await callStructured(caller, "list_workspaces", {});
23945
+ const match = (workspaces2.workspaces ?? []).find((ws) => ws.id === context.workspaceId);
23852
23946
  pinnedWorkspace = {
23853
23947
  id: context.workspaceId,
23854
23948
  name: match?.name ?? "pinned"
@@ -23861,17 +23955,28 @@ async function buildCatalog(caller, flags, detection) {
23861
23955
  ) : null;
23862
23956
  const { candidates: matched, pinned: discoveryPinned } = await resolveCandidates(caller, detection.repoOwnerName);
23863
23957
  const candidates = flagWorkspaceId ? matched.filter((candidate) => candidate.workspace.id === flagWorkspaceId) : matched;
23958
+ let workspaces = null;
23959
+ if (matched.length === 0 && !pinnedWorkspace && !flagWorkspaceId) {
23960
+ const listed = await callStructured(caller, "list_workspaces", {});
23961
+ workspaces = (listed.workspaces ?? []).map(({ id, name, slug }) => ({ id, name, slug }));
23962
+ }
23963
+ const sole = (rows) => rows.length === 1 && !isArchiveNamed(rows[0].name) ? rows[0] : null;
23964
+ const candidateWorkspaces = [
23965
+ ...new Map(candidates.map((c) => [c.workspace.id, c.workspace])).values()
23966
+ ];
23967
+ const resolvedWorkspaceId = flagWorkspaceId ?? pinnedWorkspace?.id ?? sole(workspaces ?? [])?.id ?? sole(candidateWorkspaces)?.id ?? null;
23864
23968
  let otherProjects = [];
23865
23969
  let discoveryNotice = null;
23866
- if (discoveryPinned && pinnedWorkspace) {
23970
+ if (resolvedWorkspaceId) {
23867
23971
  try {
23868
- const listed = await callStructured(caller, "list_projects", {
23869
- workspaceId: pinnedWorkspace.id
23870
- });
23871
- otherProjects = (listed.projects ?? []).filter((project) => project.archivedAt === null).map(({ id, name, slug }) => ({ id, name, slug }));
23872
- discoveryNotice = "repo discovery is unavailable for a workspace-pinned token \u2014 listing every project in the pinned workspace instead (repo match unverified)";
23972
+ otherProjects = await workspaceProjects(caller, resolvedWorkspaceId);
23973
+ if (discoveryPinned) {
23974
+ discoveryNotice = "repo discovery is unavailable for a workspace-pinned token \u2014 listing every project in the pinned workspace instead (repo match unverified)";
23975
+ }
23873
23976
  } catch {
23874
- discoveryNotice = "repo discovery is unavailable for a workspace-pinned token and the workspace project list could not be loaded \u2014 pass --project <id-or-slug>, or --new-project <name> to create one";
23977
+ if (discoveryPinned) {
23978
+ discoveryNotice = "repo discovery is unavailable for a workspace-pinned token and the workspace project list could not be loaded \u2014 pass --project <id-or-slug>, or --new-project <name> to create one";
23979
+ }
23875
23980
  }
23876
23981
  }
23877
23982
  const wantsCreate = Boolean(flags.newProject && flags.newProject.trim()) || flags.project === "__create__";
@@ -23888,68 +23993,28 @@ async function buildCatalog(caller, flags, detection) {
23888
23993
  } else if (candidates.length === 1 && matched.length === 1) {
23889
23994
  chosenProjectId = candidates[0].id;
23890
23995
  }
23891
- let openTasks = null;
23892
- let members = null;
23893
- let repoLinked = null;
23894
- let projectHasBoard = null;
23895
- if (wantsCreate) {
23896
- openTasks = [];
23897
- const workspaceId = pinnedWorkspace?.id ?? null;
23898
- if (workspaceId) {
23899
- try {
23900
- const memberResult = await callStructured(caller, "list_members", {
23901
- workspaceId
23902
- });
23903
- members = memberResult.members?.map((member) => ({
23904
- id: member.userId ?? member.id ?? "",
23905
- name: member.name,
23906
- email: member.email
23907
- })) ?? [];
23908
- } catch {
23909
- members = [];
23910
- }
23911
- } else {
23912
- members = [];
23913
- }
23914
- } else if (chosenProjectId) {
23915
- try {
23916
- const project = await projectBoards(caller, chosenProjectId);
23917
- repoLinked = repoLinkedOf(
23918
- { candidates, detection },
23919
- chosenProjectId,
23920
- project.repoOwners
23921
- );
23922
- projectHasBoard = project.boardIds.length > 0;
23923
- openTasks = await openTasksForProject(caller, project.boardIds);
23924
- const memberResult = await callStructured(caller, "list_members", {
23925
- workspaceId: project.workspaceId
23926
- });
23927
- members = memberResult.members?.map((member) => ({
23928
- id: member.userId ?? member.id ?? "",
23929
- name: member.name,
23930
- email: member.email
23931
- })) ?? null;
23932
- } catch {
23933
- }
23934
- }
23935
23996
  const chosenWorkspaceId = candidates.find((candidate) => candidate.id === chosenProjectId)?.workspace.id ?? flagWorkspaceId ?? pinnedWorkspace?.id ?? null;
23936
- return {
23997
+ const catalog = {
23937
23998
  detection,
23938
23999
  pinnedWorkspace,
23939
24000
  candidates,
23940
24001
  otherProjects,
23941
24002
  discoveryNotice,
24003
+ workspaces,
23942
24004
  chosenWorkspaceId,
23943
24005
  chosenProjectId,
23944
- openTasks,
23945
- members,
23946
- operatorUserId: null,
24006
+ openTasks: null,
24007
+ members: null,
24008
+ // D5 (STA-168): the server now says who the caller is — the owner
24009
+ // question's default, and what `--yes` falls back to. Null from an older
24010
+ // server, which keeps the explicit-owner refusal.
24011
+ operatorUserId: typeof context.userId === "string" ? context.userId : null,
23947
24012
  chosenWorkItem: flags.task ? flags.task : flags.newTask ? "__new__" : flags.sessionLevel ? "__none__" : null,
23948
24013
  chosenOwnerId: flags.owner ?? null,
23949
24014
  newTaskTitle: flags.newTask?.trim() || null,
23950
24015
  newProjectName: flags.newProject?.trim() || null,
23951
- repoLinked,
23952
- projectHasBoard,
24016
+ repoLinked: null,
24017
+ projectHasBoard: null,
23953
24018
  // STA-11: filled by runAlign (it needs the checkout root) — the ordering
23954
24019
  // preference for the workspace question.
23955
24020
  lastAlignedWorkspaceId: null,
@@ -23965,8 +24030,24 @@ async function buildCatalog(caller, flags, detection) {
23965
24030
  binding: null,
23966
24031
  mcpConfig: null,
23967
24032
  // Filled by runAlign — it needs the checkout's marker session.
23968
- priorArtifacts: null
24033
+ priorArtifacts: null,
24034
+ // D7: filled by runAlign (it needs the checkout's newest marker).
24035
+ previous: null
23969
24036
  };
24037
+ if (wantsCreate) {
24038
+ catalog.openTasks = [];
24039
+ catalog.members = [];
24040
+ if (resolvedWorkspaceId) {
24041
+ try {
24042
+ catalog.members = await workspaceMembers(caller, resolvedWorkspaceId);
24043
+ } catch {
24044
+ catalog.members = [];
24045
+ }
24046
+ }
24047
+ } else if (chosenProjectId) {
24048
+ await loadProjectContext(caller, catalog, chosenProjectId);
24049
+ }
24050
+ return catalog;
23970
24051
  }
23971
24052
  async function priorArtifactsOf(caller, markerSession) {
23972
24053
  const current = markerSession?.alignment?.task;
@@ -24323,7 +24404,8 @@ async function runAlign(flags, deps2) {
24323
24404
  flags.capture,
24324
24405
  marker !== null && markerHost !== null && isHostCapturing(deps2, marker.sessionId, markerHost)
24325
24406
  );
24326
- catalog.lastAlignedWorkspaceId = newestAlignmentMarker(deps2.configPath, inspection.root)?.workspaceId ?? null;
24407
+ const newest = newestAlignmentMarker(deps2.configPath, inspection.root);
24408
+ catalog.lastAlignedWorkspaceId = newest?.workspaceId ?? null;
24327
24409
  catalog.binding = {
24328
24410
  providerSessionId: detected.providerSessionId,
24329
24411
  transcriptPath: detected.transcriptPath,
@@ -24349,6 +24431,31 @@ async function runAlign(flags, deps2) {
24349
24431
  }
24350
24432
  }
24351
24433
  const markerActive = markerSession !== null && (markerSession.status === "ACTIVE" || markerSession.status === "STARTING");
24434
+ let workspaceFromPrevious = false;
24435
+ if (!flags.fresh && !markerActive && newest) {
24436
+ const previous = await previousAlignmentOf(
24437
+ caller,
24438
+ newest,
24439
+ // This provider session's own (ended) marker is the newest: reuse
24440
+ // the read above rather than paying for it twice.
24441
+ marker?.sessionId === newest.sessionId ? markerSession : null
24442
+ );
24443
+ if (previous) {
24444
+ const projectPrefilled = catalog.chosenProjectId === null && await loadProjectContext(caller, catalog, previous.project.id);
24445
+ if (projectPrefilled) {
24446
+ catalog.chosenProjectId = previous.project.id;
24447
+ if (catalog.chosenWorkspaceId === null) {
24448
+ catalog.chosenWorkspaceId = previous.workspaceId;
24449
+ workspaceFromPrevious = true;
24450
+ }
24451
+ }
24452
+ catalog.previous = prefillFromPrevious(
24453
+ previous,
24454
+ catalog,
24455
+ projectPrefilled
24456
+ );
24457
+ }
24458
+ }
24352
24459
  catalog.priorArtifacts = await priorArtifactsOf(caller, markerSession);
24353
24460
  const preconditions = await doctorLocalChecks(deps2);
24354
24461
  preconditions.push(sessionBindingCheck(detected));
@@ -24433,7 +24540,7 @@ async function runAlign(flags, deps2) {
24433
24540
  let pendingCreateProject = null;
24434
24541
  if (flags.newTask) workItem = `__new__:${flags.newTask}`;
24435
24542
  if (flags.newProject && flags.newProject.trim()) {
24436
- const workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id ?? null;
24543
+ const workspaceId = buildAlignQuestions(catalog)[0]?.answer ?? null;
24437
24544
  if (!workspaceId) {
24438
24545
  throw new UsageError(
24439
24546
  "--new-project needs a resolved workspace (a workspace-pinned token, a repo-matched candidate, or --workspace <id-or-slug>) \u2014 none resolved here"
@@ -24494,11 +24601,21 @@ async function runAlign(flags, deps2) {
24494
24601
  catalog.newProjectName = null;
24495
24602
  pendingCreateProject = null;
24496
24603
  createWorkspaceId = null;
24604
+ catalog.previous = null;
24605
+ if (workspaceFromPrevious) {
24606
+ catalog.chosenWorkspaceId = null;
24607
+ workspaceFromPrevious = false;
24608
+ }
24497
24609
  continue;
24498
24610
  }
24499
24611
  const answer = await askQuestion(deps2, next);
24500
24612
  if (next.id === "workspace") {
24501
24613
  catalog.chosenWorkspaceId = answer;
24614
+ try {
24615
+ catalog.otherProjects = await workspaceProjects(caller, answer);
24616
+ } catch {
24617
+ catalog.otherProjects = [];
24618
+ }
24502
24619
  } else if (next.id === "task_title") {
24503
24620
  newTaskTitle = answer;
24504
24621
  } else if (next.id === "project_name") {
@@ -24514,7 +24631,7 @@ async function runAlign(flags, deps2) {
24514
24631
  };
24515
24632
  } else if (next.id === "project") {
24516
24633
  if (answer === "__create__") {
24517
- const workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id;
24634
+ const workspaceId = questions[0]?.answer ?? null;
24518
24635
  if (!workspaceId) {
24519
24636
  throw new UsageError(
24520
24637
  "cannot create a project without a resolved workspace \u2014 pick a workspace first"
@@ -24526,16 +24643,7 @@ async function runAlign(flags, deps2) {
24526
24643
  catalog.projectHasBoard = null;
24527
24644
  catalog.openTasks = [];
24528
24645
  try {
24529
- const memberResult = await callStructured(
24530
- caller,
24531
- "list_members",
24532
- { workspaceId }
24533
- );
24534
- catalog.members = memberResult.members?.map((member) => ({
24535
- id: member.userId ?? member.id ?? "",
24536
- name: member.name,
24537
- email: member.email
24538
- })) ?? [];
24646
+ catalog.members = await workspaceMembers(caller, workspaceId);
24539
24647
  } catch {
24540
24648
  catalog.members = [];
24541
24649
  }
@@ -24543,31 +24651,7 @@ async function runAlign(flags, deps2) {
24543
24651
  } else {
24544
24652
  projectId = answer;
24545
24653
  }
24546
- try {
24547
- const project = await projectBoards(caller, projectId);
24548
- catalog.repoLinked = repoLinkedOf(
24549
- catalog,
24550
- projectId,
24551
- project.repoOwners
24552
- );
24553
- catalog.projectHasBoard = project.boardIds.length > 0;
24554
- catalog.openTasks = await openTasksForProject(
24555
- caller,
24556
- project.boardIds
24557
- );
24558
- const memberResult = await callStructured(
24559
- caller,
24560
- "list_members",
24561
- {
24562
- workspaceId: project.workspaceId
24563
- }
24564
- );
24565
- catalog.members = memberResult.members?.map((member) => ({
24566
- id: member.userId ?? member.id ?? "",
24567
- name: member.name,
24568
- email: member.email
24569
- })) ?? [];
24570
- } catch {
24654
+ if (!await loadProjectContext(caller, catalog, projectId)) {
24571
24655
  catalog.openTasks = [];
24572
24656
  catalog.members = [];
24573
24657
  catalog.repoLinked = null;
@@ -24580,10 +24664,11 @@ async function runAlign(flags, deps2) {
24580
24664
  }
24581
24665
  }
24582
24666
  }
24667
+ ownerUserId ??= (catalog.members?.length === 1 ? catalog.members[0].id : null) ?? catalog.operatorUserId;
24583
24668
  if (pendingCreateProject) {
24584
24669
  if (!ownerUserId) {
24585
24670
  throw new UsageError(
24586
- "creating a project needs an owner \u2014 answer the owner question (or pass --owner <userId>)"
24671
+ `creating a project needs an owner \u2014 answer the owner question, or pass --owner <userId> (this server did not identify you; discover members with \`jentrix tool list_members --args '{"workspaceId":"${pendingCreateProject.workspaceId}"}'\`)`
24587
24672
  );
24588
24673
  }
24589
24674
  const created = await callStructured(caller, "create_project", {
@@ -24798,6 +24883,9 @@ function registerAlignCommand(program3, deps2, onExit2) {
24798
24883
  ).option("--json", "stable JSON output").option(
24799
24884
  "--yes",
24800
24885
  "skip the re-align confirmation on an already-aligned session"
24886
+ ).option(
24887
+ "--fresh",
24888
+ "ignore this checkout's previous alignment and ask every question"
24801
24889
  ).action(async (flags) => onExit2(await runAlign(flags, deps2)));
24802
24890
  }
24803
24891
 
@@ -26871,6 +26959,7 @@ function renderHuman(report) {
26871
26959
  lines.push(
26872
26960
  ` workspace: ${tc.workspacePinned ? `pinned to ${tc.workspaceId ?? "?"}` : "not pinned (all your workspaces)"}`
26873
26961
  );
26962
+ if (tc.userId) lines.push(` user: ${tc.userId}`);
26874
26963
  if (tc.displayName) {
26875
26964
  lines.push(
26876
26965
  ` identity: ${tc.emoji ? `${tc.emoji} ` : ""}${tc.displayName}`
@@ -27031,6 +27120,7 @@ function parseTokenContext(rendered) {
27031
27120
  grandfathered: v.grandfathered,
27032
27121
  workspacePinned: v.workspacePinned,
27033
27122
  workspaceId: v.workspaceId ?? null,
27123
+ userId: typeof v.userId === "string" ? v.userId : null,
27034
27124
  displayName: v.displayName ?? null,
27035
27125
  emoji: v.emoji ?? null,
27036
27126
  rateLimit: {
@@ -28134,6 +28224,7 @@ function registerRunnerCommand(program3, deps2, onExit2) {
28134
28224
  }
28135
28225
 
28136
28226
  // src/commands/setup.ts
28227
+ import { join as join5 } from "node:path";
28137
28228
  var NPM_INSTALL_TIMEOUT_MS2 = 3e5;
28138
28229
  var SUPERSEDED = ["@jentrix/stacks-cli", "@jentrix/stacks-runner"];
28139
28230
  var CLI_SPEC = `@jentrix/cli@${CLI_VERSION}`;
@@ -28356,21 +28447,21 @@ async function bootstrapRepository(deps2) {
28356
28447
  deps2,
28357
28448
  " Jentrix aligns sessions by repository identity, so install git (https://git-scm.com/downloads) and re-run."
28358
28449
  );
28359
- return;
28450
+ return false;
28360
28451
  }
28361
28452
  const inRepo = async () => (await deps2.git(["rev-parse", "--is-inside-work-tree"], here)).code === 0;
28362
28453
  if (!await inRepo()) {
28363
28454
  const reply = await deps2.readLine(
28364
28455
  `Initialize a git repository in ${here} so agent sessions can align here? [y/N] `
28365
28456
  );
28366
- if (!/^y/i.test(reply.trim())) return;
28457
+ if (!/^y/i.test(reply.trim())) return false;
28367
28458
  if ((await deps2.git(["init"], here)).code !== 0) {
28368
28459
  say(deps2, "note: `git init` failed \u2014 skipped the repository step.");
28369
- return;
28460
+ return false;
28370
28461
  }
28371
28462
  }
28372
28463
  if ((await deps2.git(["remote", "get-url", "origin"], here)).code === 0)
28373
- return;
28464
+ return true;
28374
28465
  const fallback = here.split(/[/\\]/).filter(Boolean).pop().toLowerCase().replace(/ /g, "-");
28375
28466
  say(
28376
28467
  deps2,
@@ -28383,15 +28474,55 @@ async function bootstrapRepository(deps2) {
28383
28474
  const answer = (await deps2.readLine(
28384
28475
  "GitHub repo for origin (owner/name or full URL, blank to skip): "
28385
28476
  )).trim();
28386
- if (!answer) return;
28477
+ if (!answer) return true;
28387
28478
  const url2 = /:\/\/|^[^/]+@[^:]+:/.test(answer) ? answer : /^[^/]+\/[^/]+$/.test(answer) ? `https://github.com/${answer}.git` : null;
28388
28479
  if (!url2) {
28389
28480
  say(deps2, `skipped \u2014 "${answer}" is neither owner/name nor a URL`);
28390
- return;
28481
+ return true;
28391
28482
  }
28392
28483
  if ((await deps2.git(["remote", "add", "origin", url2], here)).code === 0) {
28393
28484
  say(deps2, `origin \u2192 ${url2}`);
28394
28485
  }
28486
+ return true;
28487
+ }
28488
+ function writeCheckoutMcpConfig(deps2, url2) {
28489
+ const path = join5(deps2.cwd(), ".mcp.json");
28490
+ if (!url2) {
28491
+ say(
28492
+ deps2,
28493
+ "note: skipped ./.mcp.json \u2014 no endpoint is known yet (not signed in, no --url); the first `jentrix align` here writes it."
28494
+ );
28495
+ return;
28496
+ }
28497
+ const raw = deps2.readTextFile(path);
28498
+ let existing = null;
28499
+ if (raw !== null) {
28500
+ try {
28501
+ existing = JSON.parse(raw);
28502
+ } catch {
28503
+ say(
28504
+ deps2,
28505
+ `note: ${path} is not valid JSON \u2014 left untouched (fix or remove it, then re-run; align refuses it the same way).`
28506
+ );
28507
+ return;
28508
+ }
28509
+ }
28510
+ const plan = planMcpServerEntry(existing, url2, null);
28511
+ if (plan.action === "unchanged") {
28512
+ say(deps2, `./.mcp.json already names ${url2}.`);
28513
+ return;
28514
+ }
28515
+ deps2.writeTextFile(path, renderMcpConfig(plan.next));
28516
+ say(
28517
+ deps2,
28518
+ plan.action === "repoint" ? `Repointed the "jentrix" MCP server in ./.mcp.json: ${plan.previousUrl} \u2192 ${url2} (Claude Code reads it at session start; no credential inside).` : `Wrote ./.mcp.json \u2192 ${url2} (Claude Code reads it at session start; no credential inside).`
28519
+ );
28520
+ if (plan.removedAuthorization) {
28521
+ say(
28522
+ deps2,
28523
+ " Removed its Authorization header: a header suppresses the client's OAuth discovery. `jentrix align --pat` puts it back for a headless checkout."
28524
+ );
28525
+ }
28395
28526
  }
28396
28527
  async function runSetupCommand(flags, deps2) {
28397
28528
  const nodeMajor = Number(process.versions.node.split(".")[0]);
@@ -28425,13 +28556,20 @@ async function runSetupCommand(flags, deps2) {
28425
28556
  const codex = await deps2.resolveCodex();
28426
28557
  if (codex) await registerCodexMcp(deps2, codex, pending);
28427
28558
  if (flags.git !== false && deps2.cwd() !== deps2.homeDir()) {
28559
+ let inWorkTree;
28428
28560
  if (deps2.isInteractive) {
28429
- await bootstrapRepository(deps2);
28430
- } else if (!await deps2.resolveGit() || (await deps2.git(["rev-parse", "--is-inside-work-tree"], deps2.cwd())).code !== 0) {
28431
- say(
28432
- deps2,
28433
- "note: skipped the repository offer (not an interactive terminal) \u2014 this folder is not a git work tree, and align requires one. Run `git init` here first."
28434
- );
28561
+ inWorkTree = await bootstrapRepository(deps2);
28562
+ } else {
28563
+ inWorkTree = await deps2.resolveGit() !== null && (await deps2.git(["rev-parse", "--is-inside-work-tree"], deps2.cwd())).code === 0;
28564
+ if (!inWorkTree) {
28565
+ say(
28566
+ deps2,
28567
+ "note: skipped the repository offer (not an interactive terminal) \u2014 this folder is not a git work tree, and align requires one. Run `git init` here first."
28568
+ );
28569
+ }
28570
+ }
28571
+ if (inWorkTree) {
28572
+ writeCheckoutMcpConfig(deps2, deps2.signedInUrl() ?? flags.url ?? null);
28435
28573
  }
28436
28574
  }
28437
28575
  say(deps2, "");
@@ -28517,7 +28655,7 @@ import {
28517
28655
  statSync as statSync2,
28518
28656
  writeFileSync as writeFileSync4
28519
28657
  } from "node:fs";
28520
- import { join as join5 } from "node:path";
28658
+ import { join as join6 } from "node:path";
28521
28659
  var MAX_SLICE_BYTES = 1e6;
28522
28660
  function captureConsented(host, marker) {
28523
28661
  if (host && typeof host.captureTrace === "boolean") return host.captureTrace;
@@ -28552,7 +28690,7 @@ function resolveSnapshotSession(payload, spoolRoot, configPath, repoRoot, readHo
28552
28690
  return null;
28553
28691
  }
28554
28692
  function offsetPath(spoolRoot, sessionId) {
28555
- return join5(spoolRoot, sessionId, "snapshot.json");
28693
+ return join6(spoolRoot, sessionId, "snapshot.json");
28556
28694
  }
28557
28695
  function readOffset(spoolRoot, sessionId) {
28558
28696
  try {
@@ -29075,7 +29213,7 @@ var sessionDeps = {
29075
29213
  closeSync3(fd);
29076
29214
  }
29077
29215
  },
29078
- spoolRoot: join6(homedir2(), ".config", "stacks", "session-spool")
29216
+ spoolRoot: join7(homedir2(), ".config", "stacks", "session-spool")
29079
29217
  };
29080
29218
  var sessionCommand = registerSessionCommand(program2, sessionDeps, onExit);
29081
29219
  registerSnapshotCommand(sessionCommand, sessionDeps, onExit);
@@ -29133,17 +29271,17 @@ registerSetupCommand(
29133
29271
  };
29134
29272
  const root = await ask(["root", "-g"]);
29135
29273
  const prefix = await ask(["prefix", "-g"]);
29136
- const binDir = prefix ? process.platform === "win32" ? prefix : join6(prefix, "bin") : null;
29274
+ const binDir = prefix ? process.platform === "win32" ? prefix : join7(prefix, "bin") : null;
29137
29275
  const dir = (name) => {
29138
29276
  if (!root) return null;
29139
- const candidate = join6(root, "@jentrix", name);
29140
- return existsSync6(join6(candidate, "package.json")) ? candidate : null;
29277
+ const candidate = join7(root, "@jentrix", name);
29278
+ return existsSync6(join7(candidate, "package.json")) ? candidate : null;
29141
29279
  };
29142
29280
  const cliDir = dir("cli");
29143
29281
  const versionOf = (packageDir) => {
29144
29282
  if (!packageDir) return null;
29145
29283
  try {
29146
- const raw = readFileSync7(join6(packageDir, "package.json"), "utf8");
29284
+ const raw = readFileSync7(join7(packageDir, "package.json"), "utf8");
29147
29285
  const parsed = JSON.parse(raw);
29148
29286
  const value = typeof parsed === "object" && parsed !== null ? parsed.version : void 0;
29149
29287
  return typeof value === "string" ? value : null;
@@ -29178,14 +29316,14 @@ registerSetupCommand(
29178
29316
  const packageRoot = persistentPluginRoot(
29179
29317
  root,
29180
29318
  PACKAGE_ROOT,
29181
- (dir) => isPluginMarketplaceDir(join6(dir, "claude-plugin"))
29319
+ (dir) => isPluginMarketplaceDir(join7(dir, "claude-plugin"))
29182
29320
  );
29183
29321
  return runPluginInstall(
29184
29322
  packageRoot === PACKAGE_ROOT ? pluginDeps : {
29185
29323
  ...pluginDeps,
29186
- resolvePluginDir: () => join6(packageRoot, "claude-plugin"),
29324
+ resolvePluginDir: () => join7(packageRoot, "claude-plugin"),
29187
29325
  resolveCodexPluginDir: () => {
29188
- const dir = join6(packageRoot, "codex-plugin");
29326
+ const dir = join7(packageRoot, "codex-plugin");
29189
29327
  return isCodexPluginMarketplaceDir(dir) ? dir : null;
29190
29328
  }
29191
29329
  },
@@ -29202,12 +29340,14 @@ registerSetupCommand(
29202
29340
  }
29203
29341
  },
29204
29342
  login: (flags) => runLoginCommand(flags, loginDeps),
29205
- codexConfigPath: () => join6(process.env.CODEX_HOME ?? join6(homedir2(), ".codex"), "config.toml"),
29343
+ codexConfigPath: () => join7(process.env.CODEX_HOME ?? join7(homedir2(), ".codex"), "config.toml"),
29206
29344
  readTextFile: (path) => existsSync6(path) ? readFileSync7(path, "utf8") : null,
29207
29345
  appendTextFile: (path, text) => {
29208
29346
  mkdirSync4(dirname4(path), { recursive: true });
29209
29347
  appendFileSync(path, text);
29210
29348
  },
29349
+ // D6 — the checkout's ./.mcp.json (OAuth mode, no credential inside).
29350
+ writeTextFile: (path, text) => writeFileSync5(path, text, { mode: 420 }),
29211
29351
  copyFile: (from, to) => copyFileSync(from, to),
29212
29352
  now: () => /* @__PURE__ */ new Date(),
29213
29353
  cwd: () => process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentrix/cli",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "description": "Command-line client for the Jentrix MCP surface (jentrix tool <name>, generated noun-verb commands).",
5
5
  "keywords": [
6
6
  "jentrix",