@tt-a1i/hive 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.en.md +5 -4
- package/README.md +1 -1
- package/dist/src/cli/hive-update.d.ts +32 -0
- package/dist/src/cli/hive-update.js +113 -22
- package/dist/src/cli/hive.d.ts +32 -0
- package/dist/src/cli/hive.js +72 -17
- package/dist/src/cli/team.js +17 -5
- package/dist/src/server/agent-command-resolver.d.ts +10 -1
- package/dist/src/server/agent-command-resolver.js +48 -4
- package/dist/src/server/agent-launch-resolver.js +9 -3
- package/dist/src/server/agent-manager-support.d.ts +28 -0
- package/dist/src/server/agent-manager-support.js +58 -4
- package/dist/src/server/agent-run-bootstrap.d.ts +17 -1
- package/dist/src/server/agent-run-bootstrap.js +30 -2
- package/dist/src/server/agent-startup-instructions.js +1 -1
- package/dist/src/server/app.d.ts +1 -0
- package/dist/src/server/app.js +12 -2
- package/dist/src/server/fs-browse.d.ts +14 -1
- package/dist/src/server/fs-browse.js +48 -5
- package/dist/src/server/fs-pick-folder.js +54 -11
- package/dist/src/server/hive-team-guidance.js +5 -4
- package/dist/src/server/open-target-commands.js +30 -4
- package/dist/src/server/post-start-input-writer.js +6 -3
- package/dist/src/server/routes-team.js +10 -1
- package/dist/src/server/runtime-store.d.ts +3 -1
- package/dist/src/server/session-capture-claude.d.ts +23 -0
- package/dist/src/server/session-capture-claude.js +24 -1
- package/dist/src/server/session-capture-opencode.d.ts +18 -0
- package/dist/src/server/session-capture-opencode.js +27 -2
- package/dist/src/server/startup-command-parser.d.ts +15 -0
- package/dist/src/server/startup-command-parser.js +33 -2
- package/dist/src/server/tasks-file-watcher.d.ts +26 -0
- package/dist/src/server/tasks-file-watcher.js +29 -3
- package/dist/src/server/team-operations.d.ts +5 -1
- package/dist/src/server/team-operations.js +44 -3
- package/dist/src/server/terminal-input-profile.js +2 -8
- package/dist/src/server/terminal-ws-server.js +26 -8
- package/package.json +2 -2
- package/web/dist/assets/{AddWorkerDialog-D-XO6MoI.js → AddWorkerDialog-DeZhTQLi.js} +2 -2
- package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +1 -0
- package/web/dist/assets/{FirstRunWizard-xHiver2Q.js → FirstRunWizard-B5wLcat5.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-CqE8_4ZP.js → MarketplaceDrawer-BC0eBOEW.js} +1 -1
- package/web/dist/assets/{WorkerModal-DgOuzZMW.js → WorkerModal-BwMHq-Bi.js} +1 -1
- package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +1 -0
- package/web/dist/assets/{WorkspaceTerminalPanels-BqEcvvSH.js → WorkspaceTerminalPanels-CvibsPSd.js} +1 -1
- package/web/dist/assets/index-Ddb7bDN5.js +75 -0
- package/web/dist/assets/path-join-S7qkXQtP.js +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/AddWorkspaceDialog-D4InpBpd.js +0 -1
- package/web/dist/assets/WorkspaceTaskDrawer-D0Y-Gyvw.js +0 -1
- package/web/dist/assets/chevron-right-Bmg7DcUj.js +0 -1
- package/web/dist/assets/index-CWW5vUjQ.js +0 -76
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
export const MAX_RUN_OUTPUT_LENGTH = 1_000_000;
|
|
3
3
|
const FORCE_KILL_DELAY_MS = 750;
|
|
4
|
+
const defaultExecRunner = (cmd, args) => {
|
|
5
|
+
execFileSync(cmd, [...args], { stdio: 'ignore', windowsHide: true });
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
|
|
9
|
+
* Windows hands `pty.kill()` to TerminateProcess against the PTY's main
|
|
10
|
+
* process only — children that the worker spawned (npm install, build
|
|
11
|
+
* scripts, custom tooling) become orphans and keep writing to the
|
|
12
|
+
* filesystem after the worker card flips to stopped.
|
|
13
|
+
*
|
|
14
|
+
* `taskkill /pid <pid> /t /f` walks the process tree (`/t`) and forces
|
|
15
|
+
* termination (`/f`), matching what task manager would do.
|
|
16
|
+
*
|
|
17
|
+
* IMPORTANT — call this BEFORE any other termination of the parent
|
|
18
|
+
* process. taskkill /T builds the tree by querying the parent for its
|
|
19
|
+
* descendants; if the parent is already gone (e.g. pty.kill() ran
|
|
20
|
+
* first) the enumeration returns empty and the children become
|
|
21
|
+
* orphans. /F also terminates the parent itself, so a parent-kill
|
|
22
|
+
* after this call is only useful as a fallback when taskkill itself
|
|
23
|
+
* failed (taskkill missing from PATH, restricted PowerShell, etc.).
|
|
24
|
+
*
|
|
25
|
+
* Best-effort: non-zero exits (process already gone, taskkill missing
|
|
26
|
+
* from PATH, access denied) are swallowed and surface as a `false`
|
|
27
|
+
* return. The caller is expected to fall back to pty.kill().
|
|
28
|
+
*
|
|
29
|
+
* Exported for unit testing — the `runner` parameter lets tests assert
|
|
30
|
+
* the exact argv without mocking node:child_process.
|
|
31
|
+
*/
|
|
32
|
+
export const taskkillProcessTree = (pid, platform = process.platform, runner = defaultExecRunner) => {
|
|
33
|
+
if (platform !== 'win32' || pid <= 0)
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
runner('taskkill', ['/pid', String(pid), '/t', '/f']);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
4
43
|
export const toAgentRunSnapshot = (run) => ({
|
|
5
44
|
runId: run.runId,
|
|
6
45
|
agentId: run.agentId,
|
|
@@ -62,8 +101,18 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
62
101
|
};
|
|
63
102
|
const killPty = (signal) => {
|
|
64
103
|
try {
|
|
65
|
-
if (process.platform === 'win32')
|
|
66
|
-
|
|
104
|
+
if (process.platform === 'win32') {
|
|
105
|
+
// taskkill /pid <pid> /t /f walks the parent's process tree
|
|
106
|
+
// BEFORE terminating it — so we have to run it while the parent
|
|
107
|
+
// is still alive. Calling pty.kill() first (the previous
|
|
108
|
+
// ordering) detaches the children: taskkill /T then fails with
|
|
109
|
+
// "process not found" and the npm-installs / build scripts
|
|
110
|
+
// become orphans. taskkill /f also terminates the parent, so
|
|
111
|
+
// pty.kill() is the fallback for the rare case where taskkill
|
|
112
|
+
// is missing from PATH or refused (e.g. restricted PowerShell).
|
|
113
|
+
if (!taskkillProcessTree(pty.pid))
|
|
114
|
+
pty.kill();
|
|
115
|
+
}
|
|
67
116
|
else
|
|
68
117
|
pty.kill(signal);
|
|
69
118
|
}
|
|
@@ -88,8 +137,13 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
88
137
|
forceKillTimer = setTimeout(() => {
|
|
89
138
|
forceKillTimer = undefined;
|
|
90
139
|
try {
|
|
91
|
-
if (process.platform === 'win32')
|
|
92
|
-
|
|
140
|
+
if (process.platform === 'win32') {
|
|
141
|
+
// Same ordering as killPty(): tree-kill before terminating the
|
|
142
|
+
// parent, so taskkill /T can still enumerate the process tree.
|
|
143
|
+
// pty.kill() is the fallback for taskkill-missing hosts.
|
|
144
|
+
if (!taskkillProcessTree(pty.pid))
|
|
145
|
+
pty.kill();
|
|
146
|
+
}
|
|
93
147
|
else
|
|
94
148
|
pty.kill('SIGKILL');
|
|
95
149
|
}
|
|
@@ -3,6 +3,23 @@ import type { AgentLaunchConfigInput } from './agent-run-store.js';
|
|
|
3
3
|
import type { AgentSessionStorePort } from './agent-runtime-ports.js';
|
|
4
4
|
import type { CommandPresetRecord } from './command-preset-store.js';
|
|
5
5
|
import { type SessionCaptureSnapshot } from './session-capture.js';
|
|
6
|
+
/**
|
|
7
|
+
* Builds a `{ <PATH-key>: <new-value> }` object for the spawn env override.
|
|
8
|
+
* Critical on Windows: the OS env block reports PATH under its native casing
|
|
9
|
+
* (typically `Path`). Writing to a literal `PATH` key would, after spread
|
|
10
|
+
* with `process.env`, leave two entries — `Path` carrying the original value
|
|
11
|
+
* and `PATH` carrying our prepend. CreateProcess then sees both and the
|
|
12
|
+
* effective lookup order is undefined; in practice the child PTY often falls
|
|
13
|
+
* back to the original `Path` and never sees `HIVE_BIN_DIR`, breaking every
|
|
14
|
+
* `team` shim resolution.
|
|
15
|
+
*
|
|
16
|
+
* We detect the existing key (case-insensitive on Windows) and overwrite IT,
|
|
17
|
+
* so the merge produces exactly one PATH entry.
|
|
18
|
+
*
|
|
19
|
+
* Exported for unit testing — `buildAgentRunBootstrap` is the only in-tree
|
|
20
|
+
* caller.
|
|
21
|
+
*/
|
|
22
|
+
export declare const buildSpawnPathEnvEntry: (parentEnv: NodeJS.ProcessEnv, hiveBinDir: string, platform: NodeJS.Platform) => NodeJS.ProcessEnv;
|
|
6
23
|
export declare const buildAgentRunBootstrap: (workspace: WorkspaceSummary, agentId: string, config: AgentLaunchConfigInput, sessionStore: AgentSessionStorePort, getCommandPreset: (id: string) => CommandPresetRecord | undefined, agent?: AgentSummary) => {
|
|
7
24
|
sessionCaptureSnapshot: {
|
|
8
25
|
discriminator?: {
|
|
@@ -50,7 +67,6 @@ export declare const buildAgentRunBootstrap: (workspace: WorkspaceSummary, agent
|
|
|
50
67
|
HIVE_PROJECT_ID: string;
|
|
51
68
|
HIVE_AGENT_ID: string;
|
|
52
69
|
HIVE_AGENT_TOKEN: string;
|
|
53
|
-
PATH: string;
|
|
54
70
|
};
|
|
55
71
|
};
|
|
56
72
|
export declare const startAgentRunCapture: ({ agentId, sessionCaptureSnapshot, sessionStore, startConfig, workspace, }: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname, posix, resolve, sep, win32 } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { buildAgentLegacyIdentityMarker, buildAgentSessionBindingMarker, } from './agent-startup-instructions.js';
|
|
4
4
|
import { withPresetResumeArgs } from './preset-launch-support.js';
|
|
@@ -12,6 +12,34 @@ const resolveHiveBinDir = () => {
|
|
|
12
12
|
};
|
|
13
13
|
const HIVE_BIN_DIR = resolveHiveBinDir();
|
|
14
14
|
const SESSION_CAPTURE_TIMEOUT_MS = 30_000;
|
|
15
|
+
/**
|
|
16
|
+
* Builds a `{ <PATH-key>: <new-value> }` object for the spawn env override.
|
|
17
|
+
* Critical on Windows: the OS env block reports PATH under its native casing
|
|
18
|
+
* (typically `Path`). Writing to a literal `PATH` key would, after spread
|
|
19
|
+
* with `process.env`, leave two entries — `Path` carrying the original value
|
|
20
|
+
* and `PATH` carrying our prepend. CreateProcess then sees both and the
|
|
21
|
+
* effective lookup order is undefined; in practice the child PTY often falls
|
|
22
|
+
* back to the original `Path` and never sees `HIVE_BIN_DIR`, breaking every
|
|
23
|
+
* `team` shim resolution.
|
|
24
|
+
*
|
|
25
|
+
* We detect the existing key (case-insensitive on Windows) and overwrite IT,
|
|
26
|
+
* so the merge produces exactly one PATH entry.
|
|
27
|
+
*
|
|
28
|
+
* Exported for unit testing — `buildAgentRunBootstrap` is the only in-tree
|
|
29
|
+
* caller.
|
|
30
|
+
*/
|
|
31
|
+
export const buildSpawnPathEnvEntry = (parentEnv, hiveBinDir, platform) => {
|
|
32
|
+
const existingKey = platform === 'win32'
|
|
33
|
+
? Object.keys(parentEnv).find((key) => key.toLowerCase() === 'path')
|
|
34
|
+
: undefined;
|
|
35
|
+
const key = existingKey ?? 'PATH';
|
|
36
|
+
const existingValue = existingKey ? parentEnv[existingKey] : parentEnv.PATH;
|
|
37
|
+
// Target platform's delimiter — Windows uses `;`, POSIX `:` — independent
|
|
38
|
+
// of where this function is running (tests on macOS verify the win32 path).
|
|
39
|
+
const platformDelimiter = platform === 'win32' ? win32.delimiter : posix.delimiter;
|
|
40
|
+
const value = existingValue ? `${hiveBinDir}${platformDelimiter}${existingValue}` : hiveBinDir;
|
|
41
|
+
return { [key]: value };
|
|
42
|
+
};
|
|
15
43
|
const resolveLaunchPreset = (config, getCommandPreset) => {
|
|
16
44
|
if (config.presetAugmentationDisabled)
|
|
17
45
|
return undefined;
|
|
@@ -52,7 +80,7 @@ export const buildAgentRunBootstrap = (workspace, agentId, config, sessionStore,
|
|
|
52
80
|
HIVE_PROJECT_ID: workspace.id,
|
|
53
81
|
HIVE_AGENT_ID: agentId,
|
|
54
82
|
HIVE_AGENT_TOKEN: '',
|
|
55
|
-
|
|
83
|
+
...buildSpawnPathEnvEntry(process.env, HIVE_BIN_DIR, process.platform),
|
|
56
84
|
},
|
|
57
85
|
};
|
|
58
86
|
};
|
|
@@ -18,7 +18,7 @@ export const buildAgentStartupInstructions = ({ agent, workspace, }) => {
|
|
|
18
18
|
lines.push('你的职责:', '- 直接响应 user,澄清需求并拆解任务', `- 维护 ${TASKS_RELATIVE_PATH}`, '- 按 worker 名称派单,并根据汇报推进下一步', '', '可用 team 命令:', '- team list', '- team send <worker-name> "<task>"', '- team cancel --dispatch <id> "<reason>"', '', '派单时必须使用 worker name,不要使用 worker id。', '取消未完成派单时必须使用 dispatch id。', '', 'Hive worker 派单规则:', ...getHiveTeamRules(agent));
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
|
-
lines.push('可用 team 命令:', '- team report "<完整汇报>" [--dispatch <id>] [--artifact <path>] 完成/失败/阻塞汇报', '- team report --stdin [--dispatch <id>] [--artifact <path>] 同上,从 stdin 读正文(适合多行/含引号/特殊字符)', '- team status "<当前状态>" [--artifact <path>] 中段进度/待命/接入状态', '- team status --stdin [--artifact <path>] 同上,从 stdin 读正文', '- team list 查看 workspace 内的 worker(含状态)', '- team --help 仅查命令用法;**不是**汇报手段', '', '语法要点:', '- 正文是第一个 positional argument,flag 顺序任意:`team report "结论" --dispatch X` 和 `team report --dispatch X "结论"` 都成立。',
|
|
21
|
+
lines.push('可用 team 命令:', '- team report "<完整汇报>" [--dispatch <id>] [--artifact <path>] 完成/失败/阻塞汇报', '- team report --stdin [--dispatch <id>] [--artifact <path>] 同上,从 stdin 读正文(适合多行/含引号/特殊字符)', '- team status "<当前状态>" [--artifact <path>] 中段进度/待命/接入状态', '- team status --stdin [--artifact <path>] 同上,从 stdin 读正文', '- team list 查看 workspace 内的 worker(含状态)', '- team --help 仅查命令用法;**不是**汇报手段', '', '语法要点:', '- 正文是第一个 positional argument,flag 顺序任意:`team report "结论" --dispatch X` 和 `team report --dispatch X "结论"` 都成立。', '- 长正文(多行 / 含引号 / shell 特殊字符)一律走 `--stdin`,通过你所在 shell 的管道喂给它(POSIX 用 quoted heredoc `<<\'EOF\' … EOF` 防止 $var/反引号展开,Windows cmd 用 `type body.txt |` 或临时文件 `< body.txt`)。`--stdin` 自己只负责"从 stdin 读",不依赖任何 shell 语法。', '- CLI 报错会同时打印 USAGE,可直接对照修正参数。', '', '完成任务后必须执行 `team report "<结论>"`。', '失败、阻塞或部分完成也用 `team report "<当前状态与原因>"` 汇报。', '没有进行中的任务时,用 `team status "<当前状态>"` 汇报接入、待命或阻塞状态。', '不要调用 team send;worker 之间不能直接派单。', '', 'Hive worker 边界:', ...getHiveTeamRules(agent));
|
|
22
22
|
}
|
|
23
23
|
lines.push('');
|
|
24
24
|
return lines.join('\n');
|
package/dist/src/server/app.d.ts
CHANGED
|
@@ -15,5 +15,6 @@ interface CreateAppOptions {
|
|
|
15
15
|
export declare const createApp: ({ store, pickFolderService, openWorkspaceService, packageVersionReader, tasksFileService, versionService, }: CreateAppOptions) => {
|
|
16
16
|
server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
17
17
|
store: RuntimeStore;
|
|
18
|
+
closeWebSockets: () => void;
|
|
18
19
|
};
|
|
19
20
|
export type { CreateAppOptions };
|
package/dist/src/server/app.js
CHANGED
|
@@ -196,6 +196,16 @@ export const createApp = ({ store, pickFolderService = pickFolder, openWorkspace
|
|
|
196
196
|
sendJson(response, 500, { error: message });
|
|
197
197
|
}
|
|
198
198
|
});
|
|
199
|
-
createTerminalWebSocketServer(server, store, tasksFileService);
|
|
200
|
-
return {
|
|
199
|
+
const wsServer = createTerminalWebSocketServer(server, store, tasksFileService);
|
|
200
|
+
return {
|
|
201
|
+
server,
|
|
202
|
+
store,
|
|
203
|
+
// Tear-down for the WebSocket layer. Callers must invoke this
|
|
204
|
+
// BEFORE awaiting `server.close()` if they want a prompt return:
|
|
205
|
+
// `server.close()` waits on every existing socket, and Node's
|
|
206
|
+
// `closeAllConnections()` does NOT terminate already-upgraded
|
|
207
|
+
// WebSocket clients. Without this hook a Ctrl+C in the Hive
|
|
208
|
+
// runtime hangs as long as any browser tab is connected.
|
|
209
|
+
closeWebSockets: wsServer.close,
|
|
210
|
+
};
|
|
201
211
|
};
|
|
@@ -22,4 +22,17 @@ export interface FsProbeResponse {
|
|
|
22
22
|
suggested_name: string;
|
|
23
23
|
}
|
|
24
24
|
export declare const browseDirectory: (requestedPath: string) => Promise<FsBrowseResponse>;
|
|
25
|
-
export
|
|
25
|
+
export interface ProbeDirectoryOptions {
|
|
26
|
+
/**
|
|
27
|
+
* When `true` (default), probe rejects paths outside `$HOME` so the
|
|
28
|
+
* in-browser FS tree can't be tricked into revealing arbitrary disk
|
|
29
|
+
* contents via a hand-crafted path string.
|
|
30
|
+
*
|
|
31
|
+
* When `false`, the sandbox check is skipped — callers who already
|
|
32
|
+
* have a user-authorized path (e.g. paths returned by the OS-native
|
|
33
|
+
* folder picker) must use this so a Windows user picking `D:\projects`
|
|
34
|
+
* isn't rejected just because their `$HOME` lives on `C:`.
|
|
35
|
+
*/
|
|
36
|
+
enforceSandbox?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare const probeDirectory: (requestedPath: string, options?: ProbeDirectoryOptions) => Promise<FsProbeResponse>;
|
|
@@ -5,6 +5,29 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import { getFsBrowseRoot, isPathWithinRoot } from './fs-sandbox.js';
|
|
6
6
|
const execFileP = promisify(execFile);
|
|
7
7
|
const GIT_BRANCH_TIMEOUT_MS = 800;
|
|
8
|
+
/**
|
|
9
|
+
* Map a filesystem rejection (from `readdir`, `stat`, etc.) to a string
|
|
10
|
+
* suitable for surfacing in the browse response. The common Windows
|
|
11
|
+
* failure paths — System Volume Information / $Recycle.Bin (EACCES),
|
|
12
|
+
* dangling junctions (EBUSY / EINVAL), paths past MAX_PATH on systems
|
|
13
|
+
* without long-path support (ENAMETOOLONG) — each get a recognizable
|
|
14
|
+
* prefix so the UI does not just show the raw errno.
|
|
15
|
+
*/
|
|
16
|
+
const formatFilesystemError = (error) => {
|
|
17
|
+
if (!(error instanceof Error))
|
|
18
|
+
return 'Failed to read directory';
|
|
19
|
+
const code = error.code;
|
|
20
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
21
|
+
return `Permission denied: ${error.message}`;
|
|
22
|
+
}
|
|
23
|
+
if (code === 'ENAMETOOLONG') {
|
|
24
|
+
return `Path is too long for this filesystem: ${error.message}`;
|
|
25
|
+
}
|
|
26
|
+
if (code === 'EBUSY' || code === 'EINVAL') {
|
|
27
|
+
return `Path is busy or unavailable: ${error.message}`;
|
|
28
|
+
}
|
|
29
|
+
return error.message;
|
|
30
|
+
};
|
|
8
31
|
const detectGitRepository = async (entryPath) => {
|
|
9
32
|
try {
|
|
10
33
|
const info = await stat(resolve(entryPath, '.git'));
|
|
@@ -49,7 +72,7 @@ export const browseDirectory = async (requestedPath) => {
|
|
|
49
72
|
return {
|
|
50
73
|
current_path: candidate,
|
|
51
74
|
entries: [],
|
|
52
|
-
error: error
|
|
75
|
+
error: formatFilesystemError(error),
|
|
53
76
|
ok: false,
|
|
54
77
|
parent_path: null,
|
|
55
78
|
root_path: rootPath,
|
|
@@ -65,7 +88,24 @@ export const browseDirectory = async (requestedPath) => {
|
|
|
65
88
|
root_path: rootPath,
|
|
66
89
|
};
|
|
67
90
|
}
|
|
68
|
-
|
|
91
|
+
let rawEntries;
|
|
92
|
+
try {
|
|
93
|
+
rawEntries = await readdir(candidate, { withFileTypes: true });
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// Windows hits this for System Volume Information, $Recycle.Bin,
|
|
97
|
+
// broken junctions, and long paths on hosts without long-path
|
|
98
|
+
// support. Returning ok:false lets the picker surface a readable
|
|
99
|
+
// message instead of crashing the HTTP handler.
|
|
100
|
+
return {
|
|
101
|
+
current_path: candidate,
|
|
102
|
+
entries: [],
|
|
103
|
+
error: formatFilesystemError(error),
|
|
104
|
+
ok: false,
|
|
105
|
+
parent_path: null,
|
|
106
|
+
root_path: rootPath,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
69
109
|
const directoryEntries = rawEntries
|
|
70
110
|
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
71
111
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -91,9 +131,12 @@ export const browseDirectory = async (requestedPath) => {
|
|
|
91
131
|
root_path: rootPath,
|
|
92
132
|
};
|
|
93
133
|
};
|
|
94
|
-
export const probeDirectory = async (requestedPath) => {
|
|
134
|
+
export const probeDirectory = async (requestedPath, options = {}) => {
|
|
135
|
+
const enforceSandbox = options.enforceSandbox ?? true;
|
|
95
136
|
const rootPath = getFsBrowseRoot();
|
|
96
|
-
const candidate =
|
|
137
|
+
const candidate = enforceSandbox
|
|
138
|
+
? resolve(rootPath, requestedPath.trim())
|
|
139
|
+
: resolve(requestedPath.trim());
|
|
97
140
|
const base = {
|
|
98
141
|
current_branch: null,
|
|
99
142
|
exists: false,
|
|
@@ -103,7 +146,7 @@ export const probeDirectory = async (requestedPath) => {
|
|
|
103
146
|
path: candidate,
|
|
104
147
|
suggested_name: candidate.split(/[\\/]/).filter(Boolean).pop() ?? '',
|
|
105
148
|
};
|
|
106
|
-
if (!isPathWithinRoot(rootPath, candidate)) {
|
|
149
|
+
if (enforceSandbox && !isPathWithinRoot(rootPath, candidate)) {
|
|
107
150
|
return base;
|
|
108
151
|
}
|
|
109
152
|
try {
|
|
@@ -5,6 +5,13 @@ import { probeDirectory } from './fs-browse.js';
|
|
|
5
5
|
const MACOS_CANCEL_PATTERNS = [/-128/, /-1743/, /user canceled/i, /execution error/i];
|
|
6
6
|
// zenity documents exit code 1 on Cancel. kdialog uses exit code 1 as well.
|
|
7
7
|
const LINUX_CANCEL_EXIT_CODES = new Set([1]);
|
|
8
|
+
// Cap how long we'll wait for a single picker invocation. A reasonable
|
|
9
|
+
// modal-dialog dwell time is well under this — the cap exists to catch
|
|
10
|
+
// genuinely wedged pickers (PowerShell startup hang under restricted
|
|
11
|
+
// execution policy, zenity hung on a missing DBus, osascript blocked on
|
|
12
|
+
// the macOS Accessibility prompt) so the HTTP request returns instead
|
|
13
|
+
// of pinning a connection forever.
|
|
14
|
+
const PICKER_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
15
|
const defaultRunCommand = (command, args, options) => new Promise((resolve) => {
|
|
9
16
|
const child = execFile(command, args, options, (error, stdout, stderr) => {
|
|
10
17
|
const errno = error;
|
|
@@ -30,10 +37,15 @@ const emptyResponse = (overrides = {}) => ({
|
|
|
30
37
|
...overrides,
|
|
31
38
|
});
|
|
32
39
|
const finalizeWithProbe = async (path) => {
|
|
33
|
-
|
|
40
|
+
// The OS-native folder picker is itself a user-authorization surface
|
|
41
|
+
// — sandboxing again here would reject any drive other than the one
|
|
42
|
+
// hosting `$HOME` (a common Windows case: `D:\projects`, `E:\code`).
|
|
43
|
+
// The in-browser FS tree (fs-browse.ts:browseDirectory) keeps its
|
|
44
|
+
// own sandbox; only the native picker bypasses it.
|
|
45
|
+
const probe = await probeDirectory(path, { enforceSandbox: false });
|
|
34
46
|
if (!probe.ok || !probe.is_dir) {
|
|
35
47
|
return emptyResponse({
|
|
36
|
-
error: 'Selected path is
|
|
48
|
+
error: 'Selected path is not a directory.',
|
|
37
49
|
path,
|
|
38
50
|
probe,
|
|
39
51
|
});
|
|
@@ -42,7 +54,7 @@ const finalizeWithProbe = async (path) => {
|
|
|
42
54
|
};
|
|
43
55
|
const macOsPick = async (run) => {
|
|
44
56
|
const script = 'POSIX path of (choose folder with prompt "Select Hive workspace")';
|
|
45
|
-
const result = await run('osascript', ['-e', script], {});
|
|
57
|
+
const result = await run('osascript', ['-e', script], { timeout: PICKER_TIMEOUT_MS });
|
|
46
58
|
if (result.spawnError?.code === 'ENOENT') {
|
|
47
59
|
return emptyResponse({ error: 'osascript is unavailable on this host.', supported: false });
|
|
48
60
|
}
|
|
@@ -65,7 +77,7 @@ const macOsPick = async (run) => {
|
|
|
65
77
|
return finalizeWithProbe(picked);
|
|
66
78
|
};
|
|
67
79
|
const linuxPick = async (run) => {
|
|
68
|
-
const result = await run('zenity', ['--file-selection', '--directory', '--title=Select Hive workspace'], {});
|
|
80
|
+
const result = await run('zenity', ['--file-selection', '--directory', '--title=Select Hive workspace'], { timeout: PICKER_TIMEOUT_MS });
|
|
69
81
|
if (result.spawnError?.code === 'ENOENT') {
|
|
70
82
|
return emptyResponse({
|
|
71
83
|
error: 'zenity not installed. Install zenity or use Advanced: paste path.',
|
|
@@ -84,16 +96,47 @@ const linuxPick = async (run) => {
|
|
|
84
96
|
return finalizeWithProbe(picked);
|
|
85
97
|
};
|
|
86
98
|
const windowsPick = async (run) => {
|
|
99
|
+
/* Hive's PowerShell child has no visible main window, so a bare
|
|
100
|
+
`$dialog.ShowDialog()` inherits the desktop as IWin32Window parent —
|
|
101
|
+
and ends up below the foreground browser in z-order. To the user
|
|
102
|
+
that looks like a hang ("Add Workspace pops up then nothing"); the
|
|
103
|
+
picker is open, just occluded.
|
|
104
|
+
|
|
105
|
+
Fix: build a TopMost invisible owner Form, `Show()` it so it has a
|
|
106
|
+
real HWND (an unshown Form has none, and ShowDialog silently falls
|
|
107
|
+
back to desktop-parent), then pass the owner to `ShowDialog($owner)`.
|
|
108
|
+
The owner inherits TopMost z-order onto the dialog. The owner itself
|
|
109
|
+
stays invisible — Opacity 0, parked at (-32000, -32000), 1x1 size,
|
|
110
|
+
no taskbar entry — so the user only sees the picker. Dispose in
|
|
111
|
+
`finally` to release the HWND each invocation.
|
|
112
|
+
|
|
113
|
+
`Add-Type -AssemblyName System.Drawing` is required because Point /
|
|
114
|
+
Size live in System.Drawing.dll, not System.Windows.Forms.dll. */
|
|
87
115
|
const script = [
|
|
88
116
|
'Add-Type -AssemblyName System.Windows.Forms',
|
|
89
|
-
'
|
|
90
|
-
'$
|
|
91
|
-
|
|
92
|
-
'$
|
|
93
|
-
'
|
|
94
|
-
|
|
117
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
118
|
+
'$owner = New-Object System.Windows.Forms.Form',
|
|
119
|
+
"$owner.FormBorderStyle = 'None'",
|
|
120
|
+
'$owner.Opacity = 0',
|
|
121
|
+
'$owner.ShowInTaskbar = $false',
|
|
122
|
+
"$owner.StartPosition = 'Manual'",
|
|
123
|
+
'$owner.Location = New-Object System.Drawing.Point(-32000, -32000)',
|
|
124
|
+
'$owner.Size = New-Object System.Drawing.Size(1, 1)',
|
|
125
|
+
'$owner.TopMost = $true',
|
|
126
|
+
'$owner.Show()',
|
|
127
|
+
'try {',
|
|
128
|
+
' $dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
129
|
+
' $dialog.Description = "Select Hive workspace"',
|
|
130
|
+
' $dialog.ShowNewFolderButton = $false',
|
|
131
|
+
' $result = $dialog.ShowDialog($owner)',
|
|
132
|
+
' if ($result -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.WriteLine($dialog.SelectedPath); exit 0 }',
|
|
133
|
+
' exit 1',
|
|
134
|
+
'} finally {',
|
|
135
|
+
' $owner.Close()',
|
|
136
|
+
' $owner.Dispose()',
|
|
137
|
+
'}',
|
|
95
138
|
].join('; ');
|
|
96
|
-
const result = await run('powershell.exe', ['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', script], {});
|
|
139
|
+
const result = await run('powershell.exe', ['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', script], { timeout: PICKER_TIMEOUT_MS });
|
|
97
140
|
if (result.spawnError?.code === 'ENOENT') {
|
|
98
141
|
return emptyResponse({
|
|
99
142
|
error: 'PowerShell is unavailable on this host. Use Advanced: paste path.',
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* abstract identity restatement.
|
|
15
15
|
*/
|
|
16
16
|
export const ORCHESTRATOR_REMINDER_TAIL = '<hive-system-reminder>\n' +
|
|
17
|
-
'You are the Hive Orchestrator. Reply by either: (a) `team send "<worker-name>" "<task>"` to dispatch follow-up work to a Hive worker, (b) `team cancel --dispatch <id> "<reason>"` to cancel an obsolete dispatch, or (c) plain text to the user. Never call your CLI\'s built-in subagent tools (Task / Explore / etc.) — they bypass Hive and will not appear in the UI.\n' +
|
|
17
|
+
'You are the Hive Orchestrator. Reply by either: (a) `team send "<worker-name>" "<task>"` to dispatch follow-up work to a Hive worker — run `team list` first when the roster may have changed since your last view (Hive does not push membership changes; stale names fail dispatch), (b) `team cancel --dispatch <id> "<reason>"` to cancel an obsolete dispatch, or (c) plain text to the user. Never call your CLI\'s built-in subagent tools (Task / Explore / etc.) — they bypass Hive and will not appear in the UI.\n' +
|
|
18
18
|
'</hive-system-reminder>';
|
|
19
19
|
/**
|
|
20
20
|
* Tail reminder appended to dispatches sent TO a worker. Reinforces the
|
|
@@ -27,7 +27,7 @@ export const buildWorkerReminderTail = (dispatchId) => '<hive-system-reminder>\n
|
|
|
27
27
|
'</hive-system-reminder>';
|
|
28
28
|
const ORCHESTRATOR_RULES = [
|
|
29
29
|
'Hive worker 是右侧卡片里的真实 CLI agent,不是你所在 CLI 的内置 subagent / 子代理工具。',
|
|
30
|
-
'
|
|
30
|
+
'派单前必须以最新 roster 为准:每次 `team send` 之前,如果距上一次 `team list` 之后发生过 user 回复或你自己的派单/取消,重新跑一次 `team list` 拿当前成员名单。Hive 不主动推送成员变更——别按对会话早期 roster 的记忆派单,否则容易派到已改名或已删除的 worker。',
|
|
31
31
|
'普通、低风险、几分钟内能直接完成的小任务可以自己做;不要为了形式感派 worker。需要并行、长时间执行、独立 review/test、专门角色,或 user 明确要求 worker/成员处理时,再用 `team send`。',
|
|
32
32
|
'如果只有一个可用 worker,直接用 `team send <worker-name> "<task>"` 派给它;不要把选择题丢回给 user。',
|
|
33
33
|
'当 user 要你“让 worker ...”时,必须用 `team send <worker-name> "<task>"` 派给 Hive worker。',
|
|
@@ -58,7 +58,8 @@ export const buildProtocolDoc = () => [
|
|
|
58
58
|
'',
|
|
59
59
|
'This file is auto-generated by Hive on every workspace open. If you',
|
|
60
60
|
'(the agent) lost context after `/compact` or an internal summarization,',
|
|
61
|
-
'
|
|
61
|
+
're-read `.hive/PROTOCOL.md` (POSIX: `cat`, Windows cmd: `type`, PowerShell:',
|
|
62
|
+
'`Get-Content`) to re-anchor.',
|
|
62
63
|
'',
|
|
63
64
|
'## You are running inside Hive',
|
|
64
65
|
'',
|
|
@@ -81,7 +82,7 @@ export const buildProtocolDoc = () => [
|
|
|
81
82
|
'## `team` CLI — worker',
|
|
82
83
|
'',
|
|
83
84
|
'- `team report "<result>" --dispatch <id>` — report task outcome',
|
|
84
|
-
|
|
85
|
+
'- `team report --stdin --dispatch <id>` — same, body from stdin (pipe content in via your shell — POSIX heredoc, `type file |`, or whatever your environment supports)',
|
|
85
86
|
'- `team status "<state>"` — update orchestrator when no dispatch is active',
|
|
86
87
|
'',
|
|
87
88
|
'## Orchestrator rules',
|
|
@@ -44,18 +44,37 @@ const linuxAttempts = (targetId, path) => {
|
|
|
44
44
|
return [{ command: 'xdg-open', args: [path] }];
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Wrap a PATHEXT-dependent shim (`code.cmd`, `cursor.cmd`, etc.) in cmd.exe
|
|
49
|
+
* so Windows resolves the `.cmd` extension. `execFile` does not consult
|
|
50
|
+
* PATHEXT and cannot launch `.cmd` files directly, so a bare `code` argv
|
|
51
|
+
* returns ENOENT even when VSCode is installed.
|
|
52
|
+
*
|
|
53
|
+
* `cmd.exe /d /s /c` parses the trailing command line. We pass the binary
|
|
54
|
+
* name and path as separate argv elements; node's child_process quotes the
|
|
55
|
+
* path when it contains whitespace, and cmd's `/s` quote-stripping does NOT
|
|
56
|
+
* fire because the command line starts with the binary name (e.g. `code`),
|
|
57
|
+
* not a `"`.
|
|
58
|
+
*/
|
|
59
|
+
const cmdExeShimAttempt = (bin, path) => ({
|
|
60
|
+
command: 'cmd.exe',
|
|
61
|
+
args: ['/d', '/s', '/c', bin, path],
|
|
62
|
+
});
|
|
47
63
|
const windowsAttempts = (targetId, path) => {
|
|
48
64
|
switch (targetId) {
|
|
49
65
|
case 'finder':
|
|
66
|
+
// explorer.exe is a real PE binary; the existing exit-code-1 success
|
|
67
|
+
// heuristic in classifyFailure depends on the spawn going directly to
|
|
68
|
+
// explorer, so do NOT wrap this in cmd.exe.
|
|
50
69
|
return [{ command: 'explorer', args: [path] }];
|
|
51
70
|
case 'vscode':
|
|
52
|
-
return [
|
|
71
|
+
return [cmdExeShimAttempt('code', path)];
|
|
53
72
|
case 'vscode-insiders':
|
|
54
|
-
return [
|
|
73
|
+
return [cmdExeShimAttempt('code-insiders', path)];
|
|
55
74
|
case 'cursor':
|
|
56
|
-
return [
|
|
75
|
+
return [cmdExeShimAttempt('cursor', path)];
|
|
57
76
|
case 'zed':
|
|
58
|
-
return [
|
|
77
|
+
return [cmdExeShimAttempt('zed', path)];
|
|
59
78
|
default:
|
|
60
79
|
return [{ command: 'explorer', args: [path] }];
|
|
61
80
|
}
|
|
@@ -94,6 +113,13 @@ const APP_NOT_INSTALLED_PATTERNS = [
|
|
|
94
113
|
/can'?t find/i,
|
|
95
114
|
/not authorized to send keystrokes/i,
|
|
96
115
|
/application can'?t be found/i,
|
|
116
|
+
// Windows: when cmd.exe can't resolve the .cmd shim (e.g. VSCode not
|
|
117
|
+
// installed), it prints this message to stderr and exits non-zero. We
|
|
118
|
+
// wrap editor invocations in `cmd /d /s /c` so the spawn itself always
|
|
119
|
+
// succeeds (cmd.exe is always present); the missing-binary signal is in
|
|
120
|
+
// the stderr text, not in spawn ENOENT.
|
|
121
|
+
/is not recognized as an internal or external command/i,
|
|
122
|
+
/不是内部或外部命令/, // zh-CN Windows localization of the above
|
|
97
123
|
];
|
|
98
124
|
const classifyFailure = (result) => {
|
|
99
125
|
if (result.spawnError?.code === 'ENOENT')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeExecutableToken } from './startup-command-parser.js';
|
|
2
2
|
const INTERACTIVE_COMMANDS = new Set(['claude', 'codex', 'gemini', 'opencode']);
|
|
3
3
|
const READY_CHECK_INTERVAL_MS = 50;
|
|
4
4
|
const READY_TIMEOUT_MS = 3000;
|
|
@@ -11,8 +11,11 @@ const PASTE_ACK_TIMEOUT_MS = 3000;
|
|
|
11
11
|
const COMMANDS_WITH_BRACKETED_PASTE = new Set(['claude', 'codex', 'opencode']);
|
|
12
12
|
export const toBracketedPasteSubmission = (text) => `\u001b[200~${text}\u001b[201~`;
|
|
13
13
|
const getSubmitAfterPasteDelayMs = (text) => Math.min(MAX_SUBMIT_AFTER_PASTE_DELAY_MS, Math.max(MIN_SUBMIT_AFTER_PASTE_DELAY_MS, Math.ceil(text.length / PASTE_CHARS_PER_DELAY_MS)));
|
|
14
|
-
export const isInteractiveAgentCommand = (command) =>
|
|
15
|
-
const
|
|
14
|
+
export const isInteractiveAgentCommand = (command) => {
|
|
15
|
+
const brand = normalizeExecutableToken(command);
|
|
16
|
+
return brand !== null && INTERACTIVE_COMMANDS.has(brand);
|
|
17
|
+
};
|
|
18
|
+
const getCommandName = (command) => normalizeExecutableToken(command) ?? '';
|
|
16
19
|
const hasGeminiPromptReady = (output) => /\bType your message\b/u.test(output);
|
|
17
20
|
export const hasInteractivePromptReady = (output, command = '') => {
|
|
18
21
|
const commandName = getCommandName(command);
|
|
@@ -27,7 +27,16 @@ export const teamRoutes = [
|
|
|
27
27
|
fromAgentId,
|
|
28
28
|
hivePort: String(request.socket.localPort ?? ''),
|
|
29
29
|
});
|
|
30
|
-
|
|
30
|
+
/* `restarted_worker` makes the silent auto-wake transparent — when
|
|
31
|
+
the worker had no active run at dispatch time, ensureWorkerRun
|
|
32
|
+
spawned a fresh PTY for it. The orchestrator's CLI prints a
|
|
33
|
+
stderr notice on this flag so the agent can explain the wake-up
|
|
34
|
+
to the user instead of being out of sync with worker state. */
|
|
35
|
+
sendJson(response, 202, {
|
|
36
|
+
dispatch_id: dispatch.id,
|
|
37
|
+
ok: true,
|
|
38
|
+
restarted_worker: dispatch.restartedWorker,
|
|
39
|
+
});
|
|
31
40
|
}),
|
|
32
41
|
route('POST', '/api/team/cancel', async ({ request, response, store }) => {
|
|
33
42
|
const body = await readJsonBody(request);
|
|
@@ -19,7 +19,9 @@ interface RuntimeStore {
|
|
|
19
19
|
renameWorker: (workspaceId: string, workerId: string, name: string) => AgentSummary;
|
|
20
20
|
recordUserInput: (workspaceId: string, orchestratorId: string, text: string) => void;
|
|
21
21
|
dispatchTask: (workspaceId: string, workerId: string, text: string, input?: DispatchTaskInput) => Promise<DispatchRecord>;
|
|
22
|
-
dispatchTaskByWorkerName: (workspaceId: string, workerName: string, text: string, input?: DispatchTaskInput) => Promise<DispatchRecord
|
|
22
|
+
dispatchTaskByWorkerName: (workspaceId: string, workerName: string, text: string, input?: DispatchTaskInput) => Promise<DispatchRecord & {
|
|
23
|
+
restartedWorker: boolean;
|
|
24
|
+
}>;
|
|
23
25
|
reportTask: (workspaceId: string, workerId: string, input?: ReportTaskInput) => ReportTaskResult;
|
|
24
26
|
statusTask: (workspaceId: string, workerId: string, input?: StatusTaskInput) => ReportTaskResult;
|
|
25
27
|
cancelTask: (workspaceId: string, dispatchId: string, input: CancelTaskInput) => ReportTaskResult;
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
export declare const getClaudeProjectsRoot: (pattern?: string) => string;
|
|
2
|
+
/**
|
|
3
|
+
* Match the directory-name encoding Claude Code itself uses for its project
|
|
4
|
+
* metadata under `~/.claude/projects/`. Empirically (probed via `claude
|
|
5
|
+
* --print "x"` in directories named with each character) Claude Code
|
|
6
|
+
* replaces *every* character outside `[A-Za-z0-9-]` with a single `-`,
|
|
7
|
+
* one-for-one, preserving literal hyphens.
|
|
8
|
+
*
|
|
9
|
+
* The previous regex `[\\/:\s]` only matched `\`, `/`, `:`, and whitespace —
|
|
10
|
+
* leaving `_`, `.`, parens, brackets, `@`, `#`, `&`, `+`, and any non-ASCII
|
|
11
|
+
* character (including CJK usernames) intact, while Claude Code's own
|
|
12
|
+
* encoder replaced them. The mismatch meant Hive looked for sessions under
|
|
13
|
+
* a different directory than the one Claude Code wrote to, so session
|
|
14
|
+
* resume silently failed for any workspace path containing those chars.
|
|
15
|
+
* Windows + CJK Windows usernames (`C:\Users\张三\project`) and
|
|
16
|
+
* underscored Windows project paths (`C:\my_project`) are the most common
|
|
17
|
+
* triggers, but the bug is cross-platform.
|
|
18
|
+
*
|
|
19
|
+
* Backward compatibility: workspaces whose path contains ONLY chars that
|
|
20
|
+
* the old regex already matched (`/\:` + whitespace) get the same encoded
|
|
21
|
+
* dirname as before — no behavior change. Workspaces with any other
|
|
22
|
+
* special chars previously had broken session resume; the fix moves them
|
|
23
|
+
* to the correct (working) directory.
|
|
24
|
+
*/
|
|
2
25
|
export declare const encodeClaudeProjectPath: (cwd: string) => string;
|
|
3
26
|
interface ClaudeSessionCaptureDiscriminator {
|
|
4
27
|
contentIncludes?: string | readonly string[];
|
|
@@ -18,7 +18,30 @@ export const getClaudeProjectsRoot = (pattern) => {
|
|
|
18
18
|
}
|
|
19
19
|
return root;
|
|
20
20
|
};
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Match the directory-name encoding Claude Code itself uses for its project
|
|
23
|
+
* metadata under `~/.claude/projects/`. Empirically (probed via `claude
|
|
24
|
+
* --print "x"` in directories named with each character) Claude Code
|
|
25
|
+
* replaces *every* character outside `[A-Za-z0-9-]` with a single `-`,
|
|
26
|
+
* one-for-one, preserving literal hyphens.
|
|
27
|
+
*
|
|
28
|
+
* The previous regex `[\\/:\s]` only matched `\`, `/`, `:`, and whitespace —
|
|
29
|
+
* leaving `_`, `.`, parens, brackets, `@`, `#`, `&`, `+`, and any non-ASCII
|
|
30
|
+
* character (including CJK usernames) intact, while Claude Code's own
|
|
31
|
+
* encoder replaced them. The mismatch meant Hive looked for sessions under
|
|
32
|
+
* a different directory than the one Claude Code wrote to, so session
|
|
33
|
+
* resume silently failed for any workspace path containing those chars.
|
|
34
|
+
* Windows + CJK Windows usernames (`C:\Users\张三\project`) and
|
|
35
|
+
* underscored Windows project paths (`C:\my_project`) are the most common
|
|
36
|
+
* triggers, but the bug is cross-platform.
|
|
37
|
+
*
|
|
38
|
+
* Backward compatibility: workspaces whose path contains ONLY chars that
|
|
39
|
+
* the old regex already matched (`/\:` + whitespace) get the same encoded
|
|
40
|
+
* dirname as before — no behavior change. Workspaces with any other
|
|
41
|
+
* special chars previously had broken session resume; the fix moves them
|
|
42
|
+
* to the correct (working) directory.
|
|
43
|
+
*/
|
|
44
|
+
export const encodeClaudeProjectPath = (cwd) => cwd.replace(/[^A-Za-z0-9-]/g, '-');
|
|
22
45
|
const listSessionIds = (cwd, projectsRoot = getDefaultProjectsRoot()) => {
|
|
23
46
|
const projectDir = join(projectsRoot, encodeClaudeProjectPath(cwd));
|
|
24
47
|
try {
|