@logicseek/hello-cc 0.1.7 → 0.1.8

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.
@@ -10,7 +10,7 @@ hcc update [--tag TAG] [--registry URL] [--dry-run]
10
10
  hcc uninstall [--purge --yes]
11
11
  ```
12
12
 
13
- `hcc update` 更新全局 npm 安装。`hcc uninstall` 移除本机 hooks 和 shims;
13
+ `hcc update` 更新全局 npm 安装。`hcc uninstall` 移除本机 hooks、shims 和 shell PATH 配置;
14
14
  只有在确定也要删除当前项目的 `.hello-cc` 数据和指导块时,才加
15
15
  `--purge --yes`。
16
16
 
@@ -86,6 +86,8 @@ hcc broadcast MESSAGE [--from ID] [--task N] [--inject]
86
86
 
87
87
  ```text
88
88
  hcc task create --title TEXT [--body TEXT] [--from ID] [--to ID] [--priority N]
89
+ hcc task dispatch --to ID --title TEXT [--body TEXT] [--from ID] [--message TEXT] [--no-inject] [--force]
90
+ hcc task dispatch --to ID --id N [--from ID] [--message TEXT] [--no-inject] [--force]
89
91
  hcc task list [--status S] [--peer ID] [--all]
90
92
  hcc task claim [--peer ID] --id N[,N] [--id N] [--ids N,N] [--force]
91
93
  hcc task takeover [--peer ID] --id N --reason TEXT [--policy any|blocked|stale|blocked-or-stale] [--stale-after SECONDS]
@@ -98,11 +100,14 @@ hcc task done [--peer ID] --id N --summary TEXT
98
100
  任务是项目共享事实。任务会一直可见,直到被标记为 `done` 或 `abandoned`。
99
101
  `task next` 会优先返回当前 peer 已经认领、运行、审查或阻塞中的任务;只有明确
100
102
  要再接 pending 任务时才使用 `--force`,并可结合 `--count N` 做显式批量认领。
101
- `task claim` 也支持重复 `--id`、逗号分隔的 `--id` 或 `--ids` 列表。明确要从
102
- 其他 owner 手里接管未完成任务时,使用 `task takeover`;它要求填写 reason,会
103
- 记录原 owner 并通知对方。需要更保守时,可加 `--policy blocked`、`stale` 或
104
- `blocked-or-stale`,要求任务符合可审计的阻塞/陈旧条件后才允许接管。默认 policy
105
- 仍是 `any`,以保持兼容。
103
+ `task claim` 也支持重复 `--id`、逗号分隔的 `--id` 或 `--ids` 列表。`task
104
+ dispatch` 是显式的一步分发:创建或指定已有任务并分配给目标 peer、发送持久任务
105
+ 消息,并且只在目标 peer 有正在运行的托管 Claude/Codex 终端时注入启动提示。只想
106
+ 发消息时使用 `--no-inject`;目标已经拥有其他活动任务但仍要注入时,才使用
107
+ `--force`。明确要从其他 owner 手里接管未完成任务时,使用 `task takeover`;它要求
108
+ 填写 reason,会记录原 owner 并通知对方。需要更保守时,可加 `--policy blocked`、
109
+ `stale` 或 `blocked-or-stale`,要求任务符合可审计的阻塞/陈旧条件后才允许接管。
110
+ 默认 policy 仍是 `any`,以保持兼容。
106
111
 
107
112
  ## 团队
108
113
 
package/docs/guide.md CHANGED
@@ -21,7 +21,7 @@ hcc update
21
21
  `hcc update --help` to see the available update options. `hcc task update`
22
22
  updates a coordination task, not the installed package.
23
23
 
24
- Remove hooks and shims from this machine:
24
+ Remove hooks, shims, and the shell PATH entry from this machine:
25
25
 
26
26
  ```bash
27
27
  hcc uninstall
@@ -83,11 +83,12 @@ with the same web console. The frontend Project selector switches between
83
83
  registered roots. Each project still keeps its own `.hello-cc/mesh.db`, so
84
84
  tasks, messages, locks, and peers stay isolated.
85
85
 
86
- After the first shim install, open a new terminal or run:
86
+ After the first shim install, open a new terminal or reload the rc file for
87
+ your shell:
87
88
 
88
- ```bash
89
- source ~/.bashrc
90
- ```
89
+ - bash: `source ~/.bashrc`
90
+ - zsh: `source ~/.zshrc`
91
+ - fish: `source ~/.config/fish/config.fish`
91
92
 
92
93
  Then direct project-local commands are wrapped automatically:
93
94
 
@@ -106,6 +107,10 @@ The wrappers create or reuse local persistent tmux terminals:
106
107
  - `hcc down` stops only the web runtime. It does not kill tmux sessions.
107
108
  - Running `hcc web` again reattaches live recorded tmux panes.
108
109
 
110
+ Linux also has richer process auto-discovery through `/proc`. On macOS, prefer
111
+ shim-launched or `hcc peer start` sessions when you want reliable terminal
112
+ control and stable peer identity.
113
+
109
114
  For local coordination without Web or shims, use:
110
115
 
111
116
  ```bash
@@ -20,7 +20,7 @@ hcc update
20
20
  `hcc update` 会执行 `npm install -g @logicseek/hello-cc@latest`。使用
21
21
  `hcc update --help` 可以查看更新选项。`hcc task update` 更新的是协作任务状态,不是安装包版本。
22
22
 
23
- 移除本机 hooks 和 shims:
23
+ 移除本机 hooks、shims 和 shell PATH 配置:
24
24
 
25
25
  ```bash
26
26
  hcc uninstall
@@ -76,11 +76,11 @@ stop: hcc down
76
76
 
77
77
  `hcc web` 使用单个全局 Web runtime。你在另一个项目目录再次运行 `hcc web` 时,不会再开一个端口,而是把那个项目注册到同一个 Web 控制台。前端 Project 下拉框可以在不同项目 root 之间切换;每个项目仍然使用自己的 `.hello-cc/mesh.db`,任务、消息、锁和 peers 不会混在一起。
78
78
 
79
- 第一次安装 shim 后,需要打开一个新终端,或执行:
79
+ 第一次安装 shim 后,打开新终端,或根据当前 shell 重新加载配置:
80
80
 
81
- ```bash
82
- source ~/.bashrc
83
- ```
81
+ - bash: `source ~/.bashrc`
82
+ - zsh: `source ~/.zshrc`
83
+ - fish: `source ~/.config/fish/config.fish`
84
84
 
85
85
  之后在这个项目目录中直接输入:
86
86
 
@@ -98,6 +98,9 @@ codex resume <session-id>
98
98
  - `hcc down` 只停止 Web runtime,不会杀掉这些 tmux 会话。
99
99
  - 重新运行 `hcc web` 后,只要 tmux pane 还活着,Web 会重新接管。
100
100
 
101
+ Linux 还可以通过 `/proc` 做更完整的进程自动发现。macOS 上如果需要可靠的终端
102
+ 控制和稳定 peer 身份,建议使用 shim 启动的会话或 `hcc peer start`。
103
+
101
104
  如果只想初始化本地通信、不启动 Web、不安装 shim,可使用:
102
105
 
103
106
  ```bash
@@ -53,6 +53,7 @@ const TIMELINE_EVENT_ALLOW = new Set([
53
53
  'task.done',
54
54
  'task.abandoned',
55
55
  'task.takeover',
56
+ 'task.dispatched',
56
57
  'team.started',
57
58
  'lock.acquired',
58
59
  'lock.released',
@@ -14,6 +14,7 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
14
14
  'HCC_NO_AUTO_INSTALL_TMUX',
15
15
  'HCC_PEER',
16
16
  'HCC_ROOT',
17
+ 'HCC_SKIP_SHIM_INSTALL',
17
18
  PROVIDER_STATE_ENV,
18
19
  'HCC_SHIM_ENSURED',
19
20
  'HCC_SHIM_NO_ATTACH',
@@ -1,4 +1,8 @@
1
+ import { shellQuoteArg } from '../../format.mjs';
2
+
1
3
  export function generateShim(hccBin, realBin, tool) {
4
+ const hccBinLiteral = shellQuoteArg(hccBin);
5
+ const realBinLiteral = shellQuoteArg(realBin);
2
6
  if (tool.resumeFlag) {
3
7
  // Claude: --resume/--continue/--session-id/--fork-session aware wrapper.
4
8
  return `#!/usr/bin/env bash
@@ -7,19 +11,22 @@ export function generateShim(hccBin, realBin, tool) {
7
11
  # Real binary: ${realBin}
8
12
 
9
13
  set -e
10
- REAL_BIN="${realBin}"
11
- HCC_BIN="${hccBin}"
14
+ REAL_BIN=${realBinLiteral}
15
+ HCC_BIN=${hccBinLiteral}
16
+ HCC_TMUX_BIN="\${HCC_TMUX_BIN:-$(command -v tmux 2>/dev/null || printf 'tmux')}"
17
+ export HCC_TMUX_BIN
12
18
 
13
19
  if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
14
20
  set +e
15
- "$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
21
+ "$HCC_BIN" shim ensure "${tool.name}" "$0" "$REAL_BIN" >/dev/null 2>&1
16
22
  ENSURE_STATUS=$?
17
23
  set -e
18
- if [ "$ENSURE_STATUS" = "75" ]; then
24
+ if [ "$ENSURE_STATUS" = "0" ] || [ "$ENSURE_STATUS" = "75" ]; then
19
25
  export HCC_SHIM_ENSURED=1
20
26
  exec "$0" "$@"
21
27
  fi
22
28
  fi
29
+ unset HCC_SHIM_ENSURED
23
30
 
24
31
  should_passthrough() {
25
32
  for arg in "$@"; do
@@ -112,7 +119,7 @@ current_tmux_pane() {
112
119
  if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
113
120
  return 1
114
121
  fi
115
- tmux list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
122
+ "$HCC_TMUX_BIN" list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
116
123
  }
117
124
 
118
125
  CURRENT_HCC_PANE=""
@@ -123,7 +130,7 @@ detect_current_hcc_pane() {
123
130
  if [ -z "$CURRENT_HCC_PANE" ]; then
124
131
  return 1
125
132
  fi
126
- CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
133
+ CURRENT_HCC_SESSION=$(command "$HCC_TMUX_BIN" display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
127
134
  case "$CURRENT_HCC_SESSION" in
128
135
  hcc-*-*) return 0 ;;
129
136
  esac
@@ -138,15 +145,20 @@ if detect_current_hcc_pane; then
138
145
  PEER_ID="$SESSION_PEER_ID"
139
146
  fi
140
147
  export HCC_PEER="$PEER_ID"
141
- "$HCC_BIN" web >/dev/null 2>&1 || true
148
+ HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web >/dev/null 2>&1 || true
142
149
  "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
143
150
  exec "$REAL_BIN" "$@"
144
151
  fi
145
152
 
153
+ HCC_PROVIDER_LAUNCH_SCRIPT='REAL_BIN="$1"; shift; HCC_TMUX_BIN="\${HCC_TMUX_BIN:-tmux}"; command "$HCC_TMUX_BIN" set-environment HCC_PROVIDER_STATE running >/dev/null 2>&1 || true; "$REAL_BIN" "$@"; command "$HCC_TMUX_BIN" set-environment HCC_PROVIDER_STATE exited >/dev/null 2>&1 || true; exec bash --noprofile --norc'
154
+
146
155
  set +e
147
156
  # Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
148
- START_OUT=$("$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc 'REAL_BIN="$1"; shift; hcc_provider_state() { HCC_TMUX_SESSION=$(tmux display-message -p "#S" 2>/dev/null || true); if [ -n "$HCC_TMUX_SESSION" ]; then tmux set-environment -t "$HCC_TMUX_SESSION" HCC_PROVIDER_STATE "$1" >/dev/null 2>&1 || true; fi; }; hcc_provider_state running; "$REAL_BIN" "$@"; hcc_provider_state exited; exec bash' _ "$REAL_BIN" "$@" 2>&1)
157
+ START_OUT_FILE=$(mktemp "\${TMPDIR:-/tmp}/hcc-shim-start.XXXXXX")
158
+ (HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc "$HCC_PROVIDER_LAUNCH_SCRIPT" _ "$REAL_BIN" "$@") >"$START_OUT_FILE" 2>&1
149
159
  START_STATUS=$?
160
+ START_OUT=$(cat "$START_OUT_FILE" 2>/dev/null || true)
161
+ rm -f "$START_OUT_FILE"
150
162
  set -e
151
163
  if [ "$START_STATUS" -ne 0 ]; then
152
164
  printf '%s\\n' "$START_OUT" >&2
@@ -164,17 +176,17 @@ if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
164
176
  exit 0
165
177
  fi
166
178
 
167
- SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
179
+ SESSION=$(command "$HCC_TMUX_BIN" display-message -p -t "$PANE" '#S' 2>/dev/null || true)
168
180
  if [ -z "$SESSION" ]; then
169
181
  printf '%s\\n' "$START_OUT"
170
182
  exit 0
171
183
  fi
172
184
 
173
185
  if [ -n "$TMUX" ]; then
174
- exec tmux switch-client -t "$SESSION"
186
+ exec "$HCC_TMUX_BIN" switch-client -t "$SESSION"
175
187
  fi
176
188
 
177
- exec tmux attach-session -t "$SESSION"
189
+ exec "$HCC_TMUX_BIN" attach-session -t "$SESSION"
178
190
  `;
179
191
  }
180
192
 
@@ -185,19 +197,22 @@ exec tmux attach-session -t "$SESSION"
185
197
  # Real binary: ${realBin}
186
198
 
187
199
  set -e
188
- REAL_BIN="${realBin}"
189
- HCC_BIN="${hccBin}"
200
+ REAL_BIN=${realBinLiteral}
201
+ HCC_BIN=${hccBinLiteral}
202
+ HCC_TMUX_BIN="\${HCC_TMUX_BIN:-$(command -v tmux 2>/dev/null || printf 'tmux')}"
203
+ export HCC_TMUX_BIN
190
204
 
191
205
  if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
192
206
  set +e
193
- "$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
207
+ "$HCC_BIN" shim ensure "${tool.name}" "$0" "$REAL_BIN" >/dev/null 2>&1
194
208
  ENSURE_STATUS=$?
195
209
  set -e
196
- if [ "$ENSURE_STATUS" = "75" ]; then
210
+ if [ "$ENSURE_STATUS" = "0" ] || [ "$ENSURE_STATUS" = "75" ]; then
197
211
  export HCC_SHIM_ENSURED=1
198
212
  exec "$0" "$@"
199
213
  fi
200
214
  fi
215
+ unset HCC_SHIM_ENSURED
201
216
 
202
217
  first_non_option() {
203
218
  EXPECT_VALUE=0
@@ -317,7 +332,7 @@ current_tmux_pane() {
317
332
  if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
318
333
  return 1
319
334
  fi
320
- tmux list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
335
+ "$HCC_TMUX_BIN" list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
321
336
  }
322
337
 
323
338
  CURRENT_HCC_PANE=""
@@ -328,7 +343,7 @@ detect_current_hcc_pane() {
328
343
  if [ -z "$CURRENT_HCC_PANE" ]; then
329
344
  return 1
330
345
  fi
331
- CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
346
+ CURRENT_HCC_SESSION=$(command "$HCC_TMUX_BIN" display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
332
347
  case "$CURRENT_HCC_SESSION" in
333
348
  hcc-*-*) return 0 ;;
334
349
  esac
@@ -343,15 +358,20 @@ if detect_current_hcc_pane; then
343
358
  PEER_ID="$SESSION_PEER_ID"
344
359
  fi
345
360
  export HCC_PEER="$PEER_ID"
346
- "$HCC_BIN" web >/dev/null 2>&1 || true
361
+ HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web >/dev/null 2>&1 || true
347
362
  "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
348
363
  exec "$REAL_BIN" "$@"
349
364
  fi
350
365
 
366
+ HCC_PROVIDER_LAUNCH_SCRIPT='REAL_BIN="$1"; shift; HCC_TMUX_BIN="\${HCC_TMUX_BIN:-tmux}"; command "$HCC_TMUX_BIN" set-environment HCC_PROVIDER_STATE running >/dev/null 2>&1 || true; "$REAL_BIN" "$@"; command "$HCC_TMUX_BIN" set-environment HCC_PROVIDER_STATE exited >/dev/null 2>&1 || true; exec bash --noprofile --norc'
367
+
351
368
  set +e
352
369
  # Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
353
- START_OUT=$("$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc 'REAL_BIN="$1"; shift; hcc_provider_state() { HCC_TMUX_SESSION=$(tmux display-message -p "#S" 2>/dev/null || true); if [ -n "$HCC_TMUX_SESSION" ]; then tmux set-environment -t "$HCC_TMUX_SESSION" HCC_PROVIDER_STATE "$1" >/dev/null 2>&1 || true; fi; }; hcc_provider_state running; "$REAL_BIN" "$@"; hcc_provider_state exited; exec bash' _ "$REAL_BIN" "$@" 2>&1)
370
+ START_OUT_FILE=$(mktemp "\${TMPDIR:-/tmp}/hcc-shim-start.XXXXXX")
371
+ (HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc "$HCC_PROVIDER_LAUNCH_SCRIPT" _ "$REAL_BIN" "$@") >"$START_OUT_FILE" 2>&1
354
372
  START_STATUS=$?
373
+ START_OUT=$(cat "$START_OUT_FILE" 2>/dev/null || true)
374
+ rm -f "$START_OUT_FILE"
355
375
  set -e
356
376
  if [ "$START_STATUS" -ne 0 ]; then
357
377
  printf '%s\\n' "$START_OUT" >&2
@@ -369,16 +389,16 @@ if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
369
389
  exit 0
370
390
  fi
371
391
 
372
- SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
392
+ SESSION=$(command "$HCC_TMUX_BIN" display-message -p -t "$PANE" '#S' 2>/dev/null || true)
373
393
  if [ -z "$SESSION" ]; then
374
394
  printf '%s\\n' "$START_OUT"
375
395
  exit 0
376
396
  fi
377
397
 
378
398
  if [ -n "$TMUX" ]; then
379
- exec tmux switch-client -t "$SESSION"
399
+ exec "$HCC_TMUX_BIN" switch-client -t "$SESSION"
380
400
  fi
381
401
 
382
- exec tmux attach-session -t "$SESSION"
402
+ exec "$HCC_TMUX_BIN" attach-session -t "$SESSION"
383
403
  `;
384
404
  }
@@ -10,8 +10,9 @@ export const SHIM_DIR = path.join(os.homedir(), '.hcc-shims');
10
10
  * Install shim scripts for `claude` and `codex` into SHIM_DIR.
11
11
  * Returns { shimDir, installed: [paths], changed: [paths], skipped: [names] }.
12
12
  */
13
- export function installShims(hccBin) {
13
+ export function installShims(hccBin, opts = {}) {
14
14
  fs.mkdirSync(SHIM_DIR, { recursive: true });
15
+ const realBins = opts.realBins && typeof opts.realBins === 'object' ? opts.realBins : {};
15
16
 
16
17
  const tools = [
17
18
  { name: 'claude', kind: 'claude', resumeFlag: '--resume' },
@@ -23,17 +24,23 @@ export function installShims(hccBin) {
23
24
  const skipped = [];
24
25
 
25
26
  for (const tool of tools) {
26
- const realBin = findRealBinary(tool.name);
27
+ const shimPath = path.join(SHIM_DIR, tool.name);
28
+ let previous = null;
29
+ try { previous = fs.readFileSync(shimPath, 'utf8'); } catch {}
30
+ const requestedRealBin = realBins[tool.name] || null;
31
+ const existingRealBin = previous ? shimRealBinFromContent(previous) : null;
32
+ const realBin = existingRealBin && fsExists(existingRealBin)
33
+ ? existingRealBin
34
+ : requestedRealBin && fsExists(requestedRealBin)
35
+ ? requestedRealBin
36
+ : findRealBinary(tool.name);
27
37
  if (!realBin) {
28
38
  skipped.push(`${tool.name} (not found on PATH)`);
29
39
  continue;
30
40
  }
31
- const shimPath = path.join(SHIM_DIR, tool.name);
32
41
  const content = generateShim(hccBin, realBin, tool);
33
- let previous = null;
34
- try { previous = fs.readFileSync(shimPath, 'utf8'); } catch {}
35
42
  if (previous !== content) {
36
- fs.writeFileSync(shimPath, content, { mode: 0o755 });
43
+ writeExecutableAtomic(shimPath, content);
37
44
  changed.push(shimPath);
38
45
  } else {
39
46
  try { fs.chmodSync(shimPath, 0o755); } catch {}
@@ -66,6 +73,20 @@ export function verifyShims() {
66
73
  );
67
74
  }
68
75
 
76
+ export function shimStatus() {
77
+ const tools = {};
78
+ for (const name of ['claude', 'codex']) {
79
+ const shimPath = path.join(SHIM_DIR, name);
80
+ tools[name] = { installed: fsExists(shimPath), path: shimPath };
81
+ }
82
+ return {
83
+ shimDir: SHIM_DIR,
84
+ tools,
85
+ installed: Object.values(tools).some((tool) => tool.installed),
86
+ complete: Object.values(tools).every((tool) => tool.installed)
87
+ };
88
+ }
89
+
69
90
  /**
70
91
  * Find the real binary for `name`, skipping the SHIM_DIR.
71
92
  * Checks PATH, then npm global bin, then common install locations.
@@ -104,3 +125,21 @@ export function findRealBinary(name) {
104
125
  function fsExists(p) {
105
126
  try { fs.accessSync(p); return true; } catch { return false; }
106
127
  }
128
+
129
+ function shimRealBinFromContent(content) {
130
+ const match = String(content || '').match(/^# Real binary: ([^\r\n]+)$/m);
131
+ return match ? match[1] : null;
132
+ }
133
+
134
+ function writeExecutableAtomic(file, content) {
135
+ const dir = path.dirname(file);
136
+ const base = path.basename(file);
137
+ const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
138
+ try {
139
+ fs.writeFileSync(tmp, content, { mode: 0o755 });
140
+ fs.renameSync(tmp, file);
141
+ } catch (err) {
142
+ try { fs.rmSync(tmp, { force: true }); } catch {}
143
+ throw err;
144
+ }
145
+ }
package/lib/setup.mjs CHANGED
@@ -13,6 +13,7 @@ export {
13
13
  findRealBinary,
14
14
  installShims,
15
15
  SHIM_DIR,
16
+ shimStatus,
16
17
  uninstallShims,
17
18
  verifyShims
18
19
  } from './integrations/shims.mjs';
@@ -2,26 +2,28 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
 
5
- export function pathEntryLine(shellType) {
6
- if (shellType === 'fish') return 'set -gx PATH $HOME/.hcc-shims $PATH # hello-cc shims';
7
- return '_hcc_shims="$HOME/.hcc-shims"; PATH="${PATH//:${_hcc_shims}:/:}"; PATH="${PATH#${_hcc_shims}:}"; PATH="${PATH%:${_hcc_shims}}"; export PATH="${_hcc_shims}:$PATH"; unset _hcc_shims # hello-cc shims';
5
+ export function pathEntryLine(shellType, position = 'final') {
6
+ const suffix = position === 'early' ? 'early' : 'final';
7
+ if (shellType === 'fish') return `set -gx PATH $HOME/.hcc-shims $PATH # hello-cc shims (${suffix})`;
8
+ return `_hcc_shims="$HOME/.hcc-shims"; PATH="\${PATH//:\${_hcc_shims}:/:}"; PATH="\${PATH#\${_hcc_shims}:}"; PATH="\${PATH%:\${_hcc_shims}}"; export PATH="\${_hcc_shims}:$PATH"; unset _hcc_shims # hello-cc shims (${suffix})`;
8
9
  }
9
10
 
10
11
  export function removePathEntryLines(content) {
11
12
  return String(content || '')
12
13
  .split('\n')
13
- .filter(line => !line.includes('.hcc-shims'))
14
+ .filter(line => !isHelloCcShimPathLine(line))
14
15
  .join('\n')
15
16
  .replace(/\n{3,}/g, '\n\n')
16
17
  .replace(/\s+$/, '');
17
18
  }
18
19
 
19
20
  export function insertPathEntry(content, shellType) {
20
- const line = pathEntryLine(shellType);
21
21
  const cleaned = removePathEntryLines(content);
22
- if (shellType === 'fish') return `${cleaned}${cleaned ? '\n' : ''}${line}\n`;
22
+ const finalLine = pathEntryLine(shellType, 'final');
23
+ if (shellType === 'fish' || shellType === 'zsh') return `${cleaned}${cleaned ? '\n' : ''}${finalLine}\n`;
23
24
 
24
25
  const lines = cleaned ? cleaned.split('\n') : [];
26
+ const earlyLine = pathEntryLine(shellType, 'early');
25
27
  let insertAt = 0;
26
28
  if (lines[0]?.startsWith('#!')) insertAt = 1;
27
29
  for (let i = insertAt; i < lines.length; i += 1) {
@@ -30,9 +32,9 @@ export function insertPathEntry(content, shellType) {
30
32
  insertAt = i;
31
33
  break;
32
34
  }
33
- lines.splice(insertAt, 0, line);
35
+ lines.splice(insertAt, 0, earlyLine);
34
36
  const withEarlyEntry = lines.join('\n').replace(/\s+$/, '');
35
- return `${withEarlyEntry}${withEarlyEntry ? '\n' : ''}${line}\n`;
37
+ return `${withEarlyEntry}${withEarlyEntry ? '\n' : ''}${finalLine}\n`;
36
38
  }
37
39
 
38
40
  export function detectShellRc() {
@@ -49,21 +51,34 @@ export function installPathEntry() {
49
51
 
50
52
  const next = insertPathEntry(content, shellType);
51
53
  const alreadyPresent = next === (content.endsWith('\n') ? content : `${content}\n`);
52
- if (!alreadyPresent) fs.writeFileSync(rcFile, next);
54
+ if (!alreadyPresent) {
55
+ fs.mkdirSync(path.dirname(rcFile), { recursive: true });
56
+ fs.writeFileSync(rcFile, next);
57
+ }
53
58
  return { rcFile, shellType, alreadyPresent };
54
59
  }
55
60
 
56
61
  export function uninstallPathEntry() {
57
62
  const { path: rcFile } = detectShellRc();
63
+ let content = '';
64
+ try {
65
+ content = fs.readFileSync(rcFile, 'utf8');
66
+ } catch (err) {
67
+ if (err?.code === 'ENOENT') return { rcFile, removed: false, missing: true };
68
+ return { rcFile, removed: false, error: 'could not read shell rc' };
69
+ }
70
+ const filtered = removePathEntryLines(content);
71
+ if (filtered === content.replace(/\s+$/, '')) {
72
+ return { rcFile, removed: false };
73
+ }
58
74
  try {
59
- const content = fs.readFileSync(rcFile, 'utf8');
60
- const filtered = content
61
- .split('\n')
62
- .filter(line => !line.includes('.hcc-shims'))
63
- .join('\n');
64
75
  fs.writeFileSync(rcFile, filtered);
65
- return { rcFile };
76
+ return { rcFile, removed: true };
66
77
  } catch {
67
- return { rcFile, error: 'could not modify shell rc' };
78
+ return { rcFile, removed: false, error: 'could not modify shell rc' };
68
79
  }
69
80
  }
81
+
82
+ function isHelloCcShimPathLine(line) {
83
+ return String(line || '').includes('# hello-cc shims');
84
+ }
package/lib/ui/help.mjs CHANGED
@@ -60,6 +60,8 @@ Environment:
60
60
 
61
61
  Usage:
62
62
  ${cliName} task create --title TEXT [--body TEXT] [--from ID] [--to ID] [--priority N]
63
+ ${cliName} task dispatch --to ID --title TEXT [--body TEXT] [--from ID] [--message TEXT] [--no-inject] [--force]
64
+ ${cliName} task dispatch --to ID --id N [--from ID] [--message TEXT] [--no-inject] [--force]
63
65
  ${cliName} task create --title TEXT --parent N [--team-role ROLE]
64
66
  ${cliName} task list [--status pending|claimed|running|review|blocked|done|abandoned] [--peer ID] [--all]
65
67
  ${cliName} task claim [--peer ID] --id N[,N] [--id N] [--ids N,N] [--force]
@@ -74,6 +76,11 @@ Messages use per-peer unread ack state; tasks do not.
74
76
  task next returns your existing claimed/running/review/blocked task before
75
77
  claiming another pending task. Use --force when intentionally taking additional
76
78
  pending tasks; combine it with --count N for explicit batch claims.
79
+ task dispatch explicitly assigns a new or existing task to one peer, sends a
80
+ durable task message, and injects the startup prompt only when the peer has a
81
+ running managed Claude/Codex terminal. Use --no-inject for message-only
82
+ dispatch; use --force only when intentionally injecting while the target already
83
+ owns another active task.
77
84
  Use task takeover when explicitly taking over a non-complete task from another
78
85
  owner; it records the previous owner, requires a reason, and notifies them.
79
86
  Use --policy blocked, stale, or blocked-or-stale to require an auditable
@@ -380,7 +387,7 @@ Usage:
380
387
  ${cliName} uninstall [--purge --yes]
381
388
 
382
389
  Stops the current project runtime and removes user-level hello-cc integrations:
383
- Claude/Codex hooks and claude/codex shims.
390
+ Claude/Codex hooks, claude/codex shims, and the shell PATH entry for shims.
384
391
 
385
392
  With --purge --yes, also removes current project data:
386
393
  .hello-cc/
@@ -388,6 +395,34 @@ With --purge --yes, also removes current project data:
388
395
  `);
389
396
  },
390
397
 
398
+ helpInstallHooks() {
399
+ console.log(`${cliName} install-hooks
400
+
401
+ Usage:
402
+ ${cliName} install-hooks
403
+ ${cliName} install-hooks --status
404
+ ${cliName} install-hooks --uninstall
405
+
406
+ Installs, checks, or removes Claude Code and Codex hook entries used by
407
+ hello-cc. This is a maintenance command; hcc up, hcc web, and hcc setup install
408
+ hooks automatically when needed.
409
+ `);
410
+ },
411
+
412
+ helpShim() {
413
+ console.log(`${cliName} shim
414
+
415
+ Usage:
416
+ ${cliName} shim install
417
+ ${cliName} shim status
418
+ ${cliName} shim uninstall
419
+
420
+ Installs, checks, or removes claude/codex shims in ~/.hcc-shims and manages the
421
+ shell PATH entry for that directory. hcc web and hcc setup install shims
422
+ automatically when provider CLIs are available on PATH.
423
+ `);
424
+ },
425
+
391
426
  helpWeb() {
392
427
  console.log(`${cliName} web
393
428
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logicseek/hello-cc",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Local control plane for coordinating Claude Code, Codex, and other coding CLI sessions.",
5
5
  "type": "module",
6
6
  "bin": {