@jentrix/cli 0.5.12 → 0.5.14

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.0"
4
+ "version": "0.5.1"
5
5
  }
@@ -59,7 +59,8 @@ and do NOT answer any question yourself.
59
59
  and move with
60
60
  `jentrix task move --task <taskId> --to-column-id <columnId>`. Use only
61
61
  the columns the board actually has — never invent a status. `/jentrix-end`
62
- moves it to a terminal column when the work is genuinely finished. A
62
+ moves it to the In review column when the work is genuinely finished
63
+ the terminal move is the operator's Accept, never yours. A
63
64
  board that never moves is a board that lies about what is in flight.
64
65
  11. If align wrote `./.mcp.json`, relay that line too: MCP servers load at
65
66
  session start, so the tools in THIS session still point wherever they
@@ -1,12 +1,12 @@
1
1
  {
2
- "description": "M20.1 \u00a715.1 \u2014 lifecycle bridge: hooks append their structured stdin payload (session_id, transcript_path, cwd) to the local session context file via `stacks-runner session-hook`. argv carries only the directory and event name \u2014 never credentials or transcript content. PreCompact/PostCompact additionally run `jentrix session snapshot`, which FOLLOWS THE CAPTURE CONSENT: capture ON preserves the transcript range about to leave the model's context; capture OFF (the MVP default) records the compaction boundary and NO content, because `capture: \"off\"` was disclosed as typed artifacts only, no transcript. Preservation is never distillation \u2014 a hook has no model turn; /jentrix-checkpoint does that. Compaction-path hooks carry an explicit 10s timeout (STA-51): they run inside the operator's /compact, and a slow artifact push must delay it by seconds, not the 60s default.",
2
+ "description": "M20.1 \u00a715.1 \u2014 lifecycle bridge: hooks append their structured stdin payload (session_id, transcript_path, cwd) to the local session context file via `stacks-runner session-hook`. argv carries only the provider and event name (the runner resolves the directory cross-platform \u2014 STA-131: a literal $HOME never expands in Windows hook commands) \u2014 never credentials or transcript content. PreCompact/PostCompact additionally run `jentrix session snapshot`, which FOLLOWS THE CAPTURE CONSENT: capture ON preserves the transcript range about to leave the model's context; capture OFF (the MVP default) records the compaction boundary and NO content, because `capture: \"off\"` was disclosed as typed artifacts only, no transcript. Preservation is never distillation \u2014 a hook has no model turn; /jentrix-checkpoint does that. Compaction-path hooks carry an explicit 10s timeout (STA-51): they run inside the operator's /compact, and a slow artifact push must delay it by seconds, not the 60s default.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/claude-sessions\" --event SessionStart"
9
+ "command": "stacks-runner session-hook --provider claude --event SessionStart"
10
10
  }
11
11
  ]
12
12
  }
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/claude-sessions\" --event SessionEnd"
19
+ "command": "stacks-runner session-hook --provider claude --event SessionEnd"
20
20
  }
21
21
  ]
22
22
  }
@@ -26,7 +26,7 @@
26
26
  "hooks": [
27
27
  {
28
28
  "type": "command",
29
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/claude-sessions\" --event PreCompact",
29
+ "command": "stacks-runner session-hook --provider claude --event PreCompact",
30
30
  "timeout": 10
31
31
  },
32
32
  {
@@ -42,7 +42,7 @@
42
42
  "hooks": [
43
43
  {
44
44
  "type": "command",
45
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/claude-sessions\" --event PostCompact",
45
+ "command": "stacks-runner session-hook --provider claude --event PostCompact",
46
46
  "timeout": 10
47
47
  },
48
48
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jentrix",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event SessionStart"
9
+ "command": "stacks-runner session-hook --provider codex --event SessionStart"
10
10
  }
11
11
  ]
12
12
  }
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event UserPromptSubmit"
19
+ "command": "stacks-runner session-hook --provider codex --event UserPromptSubmit"
20
20
  }
21
21
  ]
22
22
  }
@@ -26,7 +26,7 @@
26
26
  "hooks": [
27
27
  {
28
28
  "type": "command",
29
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event PostToolUse"
29
+ "command": "stacks-runner session-hook --provider codex --event PostToolUse"
30
30
  }
31
31
  ]
32
32
  }
@@ -36,7 +36,7 @@
36
36
  "hooks": [
37
37
  {
38
38
  "type": "command",
39
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event Stop"
39
+ "command": "stacks-runner session-hook --provider codex --event Stop"
40
40
  }
41
41
  ]
42
42
  }
@@ -46,7 +46,7 @@
46
46
  "hooks": [
47
47
  {
48
48
  "type": "command",
49
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event PreCompact",
49
+ "command": "stacks-runner session-hook --provider codex --event PreCompact",
50
50
  "timeout": 10
51
51
  },
52
52
  {
@@ -62,7 +62,7 @@
62
62
  "hooks": [
63
63
  {
64
64
  "type": "command",
65
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event PostCompact",
65
+ "command": "stacks-runner session-hook --provider codex --event PostCompact",
66
66
  "timeout": 10
67
67
  },
68
68
  {
@@ -78,7 +78,7 @@
78
78
  "hooks": [
79
79
  {
80
80
  "type": "command",
81
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event SessionEnd"
81
+ "command": "stacks-runner session-hook --provider codex --event SessionEnd"
82
82
  }
83
83
  ]
84
84
  }
package/dist/main.js CHANGED
@@ -10336,7 +10336,7 @@ import {
10336
10336
  readFileSync as readFileSync7,
10337
10337
  statSync as statSync3
10338
10338
  } from "node:fs";
10339
- import { homedir } from "node:os";
10339
+ import { homedir as homedir2 } from "node:os";
10340
10340
  import { basename as basename2, delimiter as delimiter2, dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
10341
10341
  import { fileURLToPath } from "node:url";
10342
10342
 
@@ -20593,8 +20593,8 @@ var ConfigError = class extends Error {
20593
20593
  this.exitCode = exitCode;
20594
20594
  }
20595
20595
  };
20596
- function configPathFor(homedir2) {
20597
- return join(homedir2, ".config", "stacks", "config.json");
20596
+ function configPathFor(homedir3) {
20597
+ return join(homedir3, ".config", "stacks", "config.json");
20598
20598
  }
20599
20599
  var PROJECT_CONFIG_DIR = ".stacks";
20600
20600
  function projectConfigPathFor(dir) {
@@ -20610,8 +20610,8 @@ function findProjectConfigPath(startDir, exists = existsSync) {
20610
20610
  dir = parent;
20611
20611
  }
20612
20612
  }
20613
- function resolveConfigPath(cwd, homedir2, exists = existsSync) {
20614
- return findProjectConfigPath(cwd, exists) ?? configPathFor(homedir2);
20613
+ function resolveConfigPath(cwd, homedir3, exists = existsSync) {
20614
+ return findProjectConfigPath(cwd, exists) ?? configPathFor(homedir3);
20615
20615
  }
20616
20616
  function scaffoldProjectConfigDir(dir) {
20617
20617
  const stacksDir = join(dir, PROJECT_CONFIG_DIR);
@@ -20962,7 +20962,7 @@ function refreshAccessToken(input) {
20962
20962
 
20963
20963
  // src/client.ts
20964
20964
  var CLI_NAME = "stacks-cli";
20965
- var CLI_VERSION = "0.5.12";
20965
+ var CLI_VERSION = "0.5.14";
20966
20966
  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).";
20967
20967
  var RELOGIN_MESSAGE = "OAuth session expired and could not be refreshed \u2014 run `jentrix login` to sign in again.";
20968
20968
  function originOf2(url2) {
@@ -21909,6 +21909,30 @@ import {
21909
21909
  writeFileSync as writeFileSync2
21910
21910
  } from "node:fs";
21911
21911
  import { dirname as dirname2, join as join2 } from "node:path";
21912
+
21913
+ // src/retry.ts
21914
+ async function withRateLimitRetry(fn, options) {
21915
+ const { maxRetries, maxWaitSeconds, sleep, now } = options;
21916
+ const start = now();
21917
+ let attempt = 0;
21918
+ for (; ; ) {
21919
+ const result = await fn();
21920
+ const envelope = envelopeOfResult(result);
21921
+ if (envelope?.error.code !== "RATE_LIMITED") return result;
21922
+ const retryAfter = envelope.error.retryAfterSeconds;
21923
+ if (typeof retryAfter !== "number" || !Number.isFinite(retryAfter)) {
21924
+ return result;
21925
+ }
21926
+ if (retryAfter < 0) return result;
21927
+ if (attempt >= maxRetries) return result;
21928
+ const elapsedMs = now() - start;
21929
+ if (elapsedMs + retryAfter * 1e3 > maxWaitSeconds * 1e3) return result;
21930
+ await sleep(retryAfter * 1e3);
21931
+ attempt += 1;
21932
+ }
21933
+ }
21934
+
21935
+ // src/commands/session.ts
21912
21936
  var LEGACY_MARKER_KEY = "__v1__";
21913
21937
  function isMarker(value) {
21914
21938
  return typeof value === "object" && value !== null && typeof value.sessionId === "string";
@@ -22029,9 +22053,28 @@ function isRecord5(value) {
22029
22053
  async function callStructured(caller, name, args) {
22030
22054
  return call(caller, name, args);
22031
22055
  }
22056
+ var ToolCallError = class extends Error {
22057
+ constructor(message, code = null) {
22058
+ super(message);
22059
+ this.code = code;
22060
+ }
22061
+ code;
22062
+ };
22032
22063
  async function call(caller, name, args) {
22033
- const result = await caller.callTool({ name, arguments: args });
22064
+ const result = await withRateLimitRetry(
22065
+ () => caller.callTool({ name, arguments: args }),
22066
+ {
22067
+ maxRetries: 2,
22068
+ maxWaitSeconds: 60,
22069
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
22070
+ now: Date.now
22071
+ }
22072
+ );
22034
22073
  if (isRecord5(result) && result.isError === true) {
22074
+ const envelope = envelopeOfResult(result);
22075
+ if (envelope) {
22076
+ throw new ToolCallError(envelope.error.message, envelope.error.code);
22077
+ }
22035
22078
  let message = "tool call failed";
22036
22079
  if (Array.isArray(result.content)) {
22037
22080
  const text = result.content.find(
@@ -22046,7 +22089,7 @@ async function call(caller, name, args) {
22046
22089
  }
22047
22090
  }
22048
22091
  }
22049
- throw new Error(message);
22092
+ throw new ToolCallError(message);
22050
22093
  }
22051
22094
  return isRecord5(result) && isRecord5(result.structuredContent) ? result.structuredContent : {};
22052
22095
  }
@@ -23573,6 +23616,26 @@ function registerSessionCommand(program3, deps2, onExit2) {
23573
23616
  }
23574
23617
 
23575
23618
  // src/commands/align.ts
23619
+ async function resolveWorkspaceFlag(caller, wanted, pinned) {
23620
+ const listed = await callStructured(caller, "list_workspaces", {});
23621
+ const rows = listed.workspaces ?? [];
23622
+ const match = rows.find(
23623
+ (ws) => ws.id === wanted || ws.slug === wanted.toLowerCase()
23624
+ );
23625
+ if (!match) {
23626
+ throw new UsageError(
23627
+ `--workspace ${wanted} matches none of your workspaces (slugs: ${rows.map((ws) => ws.slug).filter(Boolean).join(", ") || "none visible"})`,
23628
+ EXIT_CODES.NOT_FOUND
23629
+ );
23630
+ }
23631
+ if (pinned && pinned.id !== match.id) {
23632
+ throw new UsageError(
23633
+ `--workspace ${wanted} conflicts with this token's pinned workspace (${pinned.id})`,
23634
+ EXIT_CODES.FORBIDDEN
23635
+ );
23636
+ }
23637
+ return match.id;
23638
+ }
23576
23639
  var MAX_WORK_ITEMS = 15;
23577
23640
  async function projectBoards(caller, projectId) {
23578
23641
  const project = await callStructured(caller, "get_project", { projectId });
@@ -24364,10 +24427,17 @@ async function runAlign(flags, deps2) {
24364
24427
  let pendingCreateProject = null;
24365
24428
  if (flags.newTask) workItem = `__new__:${flags.newTask}`;
24366
24429
  if (flags.newProject && flags.newProject.trim()) {
24367
- const workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id ?? null;
24430
+ let workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id ?? null;
24431
+ if (flags.workspace?.trim()) {
24432
+ workspaceId = await resolveWorkspaceFlag(
24433
+ caller,
24434
+ flags.workspace.trim(),
24435
+ catalog.pinnedWorkspace ?? null
24436
+ );
24437
+ }
24368
24438
  if (!workspaceId) {
24369
24439
  throw new UsageError(
24370
- "--new-project needs a resolved workspace (a workspace-pinned token or a repo-matched candidate) \u2014 none resolved here"
24440
+ "--new-project needs a resolved workspace (a workspace-pinned token, a repo-matched candidate, or --workspace <id-or-slug>) \u2014 none resolved here"
24371
24441
  );
24372
24442
  }
24373
24443
  pendingCreateProject = {
@@ -24694,6 +24764,9 @@ function registerAlignCommand(program3, deps2, onExit2) {
24694
24764
  ).option("--project <id-or-slug>", "project to align to").option(
24695
24765
  "--new-project <name>",
24696
24766
  "create a project in the resolved workspace (seeds a board + repo/board links), then align to it"
24767
+ ).option(
24768
+ "--workspace <id-or-slug>",
24769
+ "workspace for --new-project when neither a pinned token nor a repo match resolves one (fresh checkout, non-interactive)"
24697
24770
  ).option("--task <taskId>", "existing work item to align to").option(
24698
24771
  "--new-task <title>",
24699
24772
  "create a task (existing ops core) and align to it"
@@ -24944,7 +25017,20 @@ async function basisBlock(refs, deps2) {
24944
25017
  });
24945
25018
  const record3 = result.artifact ?? result;
24946
25019
  titles.set(id, String(record3.title ?? ""));
24947
- } catch {
25020
+ } catch (error2) {
25021
+ const code = error2 instanceof ToolCallError ? error2.code : null;
25022
+ if (code === "RATE_LIMITED") {
25023
+ throw new UsageError(
25024
+ `--basis ${id} could not be verified \u2014 the server is rate limiting reads; the ref may be fine. Retry the push shortly.`,
25025
+ EXIT_CODES.RATE_LIMITED
25026
+ );
25027
+ }
25028
+ if (code && code !== "NOT_FOUND" && code !== "FORBIDDEN") {
25029
+ throw new UsageError(
25030
+ `--basis ${id} could not be verified (${code}) \u2014 the ref may be fine. Retry the push.`,
25031
+ EXIT_CODES.INTERNAL
25032
+ );
25033
+ }
24948
25034
  throw new UsageError(
24949
25035
  `--basis ${id} does not resolve to an artifact you can read \u2014 push the context first (\`jentrix push context\`), or pass a URL`,
24950
25036
  EXIT_CODES.NOT_FOUND
@@ -25019,11 +25105,13 @@ async function offerCardMint(kind, payload, flags, deps2, sessionId = null) {
25019
25105
  artifactId: payload.artifactId,
25020
25106
  anchorTaskId: payload.taskId,
25021
25107
  noun: mintNoun(ARTIFACT_TYPE_BY_PUSH_KIND[kind] ?? "FINDINGS"),
25108
+ // STA-128: the operator's own claim title, when one was given.
25109
+ artifactTitle: flags.title?.trim() || null,
25022
25110
  blocks: Boolean(flags.blocks),
25023
25111
  sessionId
25024
25112
  });
25025
25113
  deps2.writeOut(
25026
- `Minted ${minted.key ?? minted.id} on ${board.name} \xB7 artifact linked \xB7 ${flags.blocks ? "BLOCKS" : "RELATES_TO"} ${payload.taskId}`
25114
+ `Minted ${minted.key ?? minted.id} on ${board.name} \xB7 ${minted.artifactLinked ? "artifact linked" : "artifact NOT linked (see note)"} \xB7 ${flags.blocks ? "BLOCKS" : "RELATES_TO"} ${payload.taskId}`
25027
25115
  );
25028
25116
  } catch (error2) {
25029
25117
  deps2.writeErr(
@@ -25061,14 +25149,16 @@ async function mintCard(deps2, input) {
25061
25149
  const anchor = await callStructured(caller, "get_task", {
25062
25150
  taskId: input.anchorTaskId
25063
25151
  });
25152
+ const claim = input.artifactTitle?.trim();
25064
25153
  const created = await callStructured(caller, "create_task", {
25065
25154
  columnId: input.board.columnId,
25066
- title: `${input.noun} from ${String(anchor.key ?? input.anchorTaskId)}`,
25155
+ // The task-title cap is 200 chars; the claim form truncates into it.
25156
+ title: (claim ? `${input.noun}: ${claim}` : `${input.noun} from ${String(anchor.key ?? input.anchorTaskId)}`).slice(0, 200),
25067
25157
  description: `Minted from a ${input.noun.toLowerCase()} artifact pushed on ${String(anchor.key ?? input.anchorTaskId)}. Origin session artifact: ${input.artifactId}.`,
25068
25158
  idempotencyKey: `mint-${input.artifactId}`
25069
25159
  });
25070
25160
  const issueId = String(created.id);
25071
- await (deps2.fetchImpl ?? fetch)(
25161
+ const linkResponse = await (deps2.fetchImpl ?? fetch)(
25072
25162
  new URL(taskTarget(issueId).path, stacksBaseUrlOf(target.url)),
25073
25163
  {
25074
25164
  method: "POST",
@@ -25080,16 +25170,29 @@ async function mintCard(deps2, input) {
25080
25170
  body: JSON.stringify({ refArtifactId: input.artifactId })
25081
25171
  }
25082
25172
  );
25173
+ const artifactLinked = linkResponse.ok;
25174
+ if (!artifactLinked) {
25175
+ deps2.writeErr(
25176
+ `note: minted the card but linking artifact ${input.artifactId} to it failed (HTTP ${linkResponse.status}) \u2014 link it by hand: jentrix push ${pushKindForNoun(input.noun)} --task ${issueId} --ref ${input.artifactId}`
25177
+ );
25178
+ }
25083
25179
  await callStructured(caller, "add_task_link", {
25084
25180
  fromTaskId: issueId,
25085
25181
  toTaskId: input.anchorTaskId,
25086
25182
  kind: input.blocks ? "BLOCKS" : "RELATES_TO"
25087
25183
  });
25088
- return { id: issueId, key: created.key };
25184
+ return {
25185
+ id: issueId,
25186
+ key: created.key,
25187
+ artifactLinked
25188
+ };
25089
25189
  },
25090
25190
  input.sessionId ? { sessionId: input.sessionId } : void 0
25091
25191
  );
25092
25192
  }
25193
+ function pushKindForNoun(noun) {
25194
+ return noun === "Gap" ? "gap" : noun === "Issue" ? "issue" : "findings";
25195
+ }
25093
25196
  async function runMintIssue(flags, deps2) {
25094
25197
  try {
25095
25198
  let sessionId = null;
@@ -25126,6 +25229,8 @@ async function runMintIssue(flags, deps2) {
25126
25229
  artifactId: flags.artifact,
25127
25230
  anchorTaskId: flags.fromTask,
25128
25231
  noun: mintNoun(type),
25232
+ // STA-128: the stored artifact's claim title rides onto the card.
25233
+ artifactTitle: typeof record3.title === "string" ? record3.title : null,
25129
25234
  blocks: Boolean(flags.blocks),
25130
25235
  sessionId
25131
25236
  });
@@ -25135,9 +25240,10 @@ async function runMintIssue(flags, deps2) {
25135
25240
  issueKey: minted.key ?? null,
25136
25241
  boardId: board.id,
25137
25242
  artifactId: flags.artifact,
25243
+ artifactLinked: minted.artifactLinked,
25138
25244
  relatedTaskId: flags.fromTask,
25139
25245
  linkKind: flags.blocks ? "BLOCKS" : "RELATES_TO"
25140
- }) : `Minted ${minted.key ?? minted.id} on ${board.name} \xB7 artifact linked \xB7 ${flags.blocks ? "BLOCKS" : "RELATES_TO"} ${flags.fromTask}`
25246
+ }) : `Minted ${minted.key ?? minted.id} on ${board.name} \xB7 ${minted.artifactLinked ? "artifact linked" : "artifact NOT linked (see note)"} \xB7 ${flags.blocks ? "BLOCKS" : "RELATES_TO"} ${flags.fromTask}`
25141
25247
  );
25142
25248
  return 0;
25143
25249
  } catch (error2) {
@@ -26020,28 +26126,6 @@ function cellText(value) {
26020
26126
  return text;
26021
26127
  }
26022
26128
 
26023
- // src/retry.ts
26024
- async function withRateLimitRetry(fn, options) {
26025
- const { maxRetries, maxWaitSeconds, sleep, now } = options;
26026
- const start = now();
26027
- let attempt = 0;
26028
- for (; ; ) {
26029
- const result = await fn();
26030
- const envelope = envelopeOfResult(result);
26031
- if (envelope?.error.code !== "RATE_LIMITED") return result;
26032
- const retryAfter = envelope.error.retryAfterSeconds;
26033
- if (typeof retryAfter !== "number" || !Number.isFinite(retryAfter)) {
26034
- return result;
26035
- }
26036
- if (retryAfter < 0) return result;
26037
- if (attempt >= maxRetries) return result;
26038
- const elapsedMs = now() - start;
26039
- if (elapsedMs + retryAfter * 1e3 > maxWaitSeconds * 1e3) return result;
26040
- await sleep(retryAfter * 1e3);
26041
- attempt += 1;
26042
- }
26043
- }
26044
-
26045
26129
  // src/call.ts
26046
26130
  async function callTool(client, name, args, options) {
26047
26131
  let result;
@@ -27544,6 +27628,7 @@ function registerPluginCommand(program3, deps2, onExit2) {
27544
27628
  // src/commands/runner.ts
27545
27629
  import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
27546
27630
  import { constants, promises as fs } from "node:fs";
27631
+ import { homedir } from "node:os";
27547
27632
  import { delimiter, isAbsolute, resolve as resolve3 } from "node:path";
27548
27633
  var CLI_RUNNER_PROTOCOL_VERSION = 1;
27549
27634
  var RUNNER_TIMEOUT_MS = 35e3;
@@ -27577,7 +27662,10 @@ function renderOperatorIdentity(operator, workspaceName) {
27577
27662
  }
27578
27663
  async function executable(path) {
27579
27664
  try {
27580
- await fs.access(path, constants.X_OK);
27665
+ await fs.access(
27666
+ path,
27667
+ process.platform === "win32" ? constants.F_OK : constants.X_OK
27668
+ );
27581
27669
  return await fs.realpath(path);
27582
27670
  } catch {
27583
27671
  return null;
@@ -27609,6 +27697,16 @@ async function resolveRunnerExecutable(env = process.env) {
27609
27697
  }
27610
27698
  return null;
27611
27699
  }
27700
+ async function resolveClaudeExecutable(env = process.env, home = homedir()) {
27701
+ const names = platformExecutableNames("claude");
27702
+ const onPath = await resolveExecutableOnPath(names, env);
27703
+ if (onPath) return onPath;
27704
+ for (const name of names) {
27705
+ const found = await executable(resolve3(home, ".local", "bin", name));
27706
+ if (found) return found;
27707
+ }
27708
+ return null;
27709
+ }
27612
27710
  function invokeRunnerProcess(file, args, stdin, timeoutMs = RUNNER_TIMEOUT_MS) {
27613
27711
  if (!isAbsolute(file)) {
27614
27712
  return Promise.reject(new Error("runner executable path must be absolute"));
@@ -28132,8 +28230,9 @@ var RUNTIMES = [
28132
28230
  provider: "claude",
28133
28231
  resolve: (deps2) => deps2.resolveClaude(),
28134
28232
  absent: [
28135
- "note: the `claude` CLI is not installed \u2014 skipped its plugin.",
28136
- " Install Claude Code (https://claude.com/claude-code), then run: jentrix plugin install"
28233
+ "note: the `claude` CLI was not found \u2014 skipped its plugin.",
28234
+ " Installed already? This terminal's PATH may predate the install \u2014 reopen it and re-run, or run: jentrix plugin install claude",
28235
+ " Otherwise install Claude Code (https://claude.com/claude-code) first."
28137
28236
  ]
28138
28237
  },
28139
28238
  {
@@ -28302,8 +28401,15 @@ async function runSetupCommand(flags, deps2) {
28302
28401
  }
28303
28402
  const codex = await deps2.resolveCodex();
28304
28403
  if (codex) await registerCodexMcp(deps2, codex, pending);
28305
- if (flags.git !== false && deps2.isInteractive && deps2.cwd() !== deps2.homeDir()) {
28306
- await bootstrapRepository(deps2);
28404
+ if (flags.git !== false && deps2.cwd() !== deps2.homeDir()) {
28405
+ if (deps2.isInteractive) {
28406
+ await bootstrapRepository(deps2);
28407
+ } else if (!await deps2.resolveGit() || (await deps2.git(["rev-parse", "--is-inside-work-tree"], deps2.cwd())).code !== 0) {
28408
+ say(
28409
+ deps2,
28410
+ "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."
28411
+ );
28412
+ }
28307
28413
  }
28308
28414
  say(deps2, "");
28309
28415
  if (pending.failedPlugins.length) {
@@ -28725,8 +28831,8 @@ function readStdin() {
28725
28831
  process.stdin.on("error", reject);
28726
28832
  });
28727
28833
  }
28728
- var CONFIG_PATH = resolveConfigPath(process.cwd(), homedir());
28729
- var HOME_CONFIG_PATH = configPathFor(homedir());
28834
+ var CONFIG_PATH = resolveConfigPath(process.cwd(), homedir2());
28835
+ var HOME_CONFIG_PATH = configPathFor(homedir2());
28730
28836
  function configFile() {
28731
28837
  return readConfigFile(CONFIG_PATH);
28732
28838
  }
@@ -28945,7 +29051,7 @@ var sessionDeps = {
28945
29051
  closeSync3(fd);
28946
29052
  }
28947
29053
  },
28948
- spoolRoot: join6(homedir(), ".config", "stacks", "session-spool")
29054
+ spoolRoot: join6(homedir2(), ".config", "stacks", "session-spool")
28949
29055
  };
28950
29056
  var sessionCommand = registerSessionCommand(program2, sessionDeps, onExit);
28951
29057
  registerSnapshotCommand(sessionCommand, sessionDeps, onExit);
@@ -28959,7 +29065,9 @@ var CODEX_PLUGIN_DIR = fileURLToPath(
28959
29065
  var pluginDeps = {
28960
29066
  resolvePluginDir: () => existsSync6(PLUGIN_DIR) && isPluginMarketplaceDir(PLUGIN_DIR) ? PLUGIN_DIR : null,
28961
29067
  resolveCodexPluginDir: () => existsSync6(CODEX_PLUGIN_DIR) && isCodexPluginMarketplaceDir(CODEX_PLUGIN_DIR) ? CODEX_PLUGIN_DIR : null,
28962
- resolveClaude: () => resolveExecutableOnPath(platformExecutableNames("claude"), process.env),
29068
+ // STA-133: PATH plus the native installer's ~/.local/bin, so a terminal
29069
+ // whose PATH predates the Claude Code install still finds the binary.
29070
+ resolveClaude: () => resolveClaudeExecutable(process.env),
28963
29071
  resolveCodex: () => resolveExecutableOnPath(platformExecutableNames("codex"), process.env),
28964
29072
  resolveRunner: () => resolveRunnerExecutable(process.env),
28965
29073
  resolveNpm: () => resolveExecutableOnPath(platformExecutableNames("npm"), process.env),
@@ -29070,7 +29178,7 @@ registerSetupCommand(
29070
29178
  }
29071
29179
  },
29072
29180
  login: (flags) => runLoginCommand(flags, loginDeps),
29073
- codexConfigPath: () => join6(process.env.CODEX_HOME ?? join6(homedir(), ".codex"), "config.toml"),
29181
+ codexConfigPath: () => join6(process.env.CODEX_HOME ?? join6(homedir2(), ".codex"), "config.toml"),
29074
29182
  readTextFile: (path) => existsSync6(path) ? readFileSync7(path, "utf8") : null,
29075
29183
  appendTextFile: (path, text) => {
29076
29184
  mkdirSync4(dirname4(path), { recursive: true });
@@ -29079,7 +29187,7 @@ registerSetupCommand(
29079
29187
  copyFile: (from, to) => copyFileSync(from, to),
29080
29188
  now: () => /* @__PURE__ */ new Date(),
29081
29189
  cwd: () => process.cwd(),
29082
- homeDir: () => homedir(),
29190
+ homeDir: () => homedir2(),
29083
29191
  git: defaultGitRunner,
29084
29192
  readLine
29085
29193
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentrix/cli",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "Command-line client for the Jentrix MCP surface (jentrix tool <name>, generated noun-verb commands).",
5
5
  "keywords": [
6
6
  "jentrix",