@tt-a1i/hive 1.1.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.en.md +33 -0
  3. package/README.md +16 -0
  4. package/dist/src/cli/hive-update.d.ts +15 -0
  5. package/dist/src/cli/hive-update.js +81 -0
  6. package/dist/src/cli/hive.js +21 -5
  7. package/dist/src/server/agent-run-store.d.ts +1 -1
  8. package/dist/src/server/app.d.ts +3 -1
  9. package/dist/src/server/app.js +14 -1
  10. package/dist/src/server/open-target-commands.d.ts +53 -0
  11. package/dist/src/server/open-target-commands.js +176 -0
  12. package/dist/src/server/package-version.d.ts +15 -0
  13. package/dist/src/server/package-version.js +15 -0
  14. package/dist/src/server/route-types.d.ts +6 -0
  15. package/dist/src/server/routes-open-workspace.d.ts +2 -0
  16. package/dist/src/server/routes-open-workspace.js +47 -0
  17. package/dist/src/server/routes.js +2 -0
  18. package/dist/src/server/version-service.js +4 -1
  19. package/dist/src/server/workspace-shell-runtime.js +34 -8
  20. package/dist/src/shared/open-targets.d.ts +20 -0
  21. package/dist/src/shared/open-targets.js +36 -0
  22. package/package.json +3 -3
  23. package/web/dist/assets/finder-C4Jmsb0B.png +0 -0
  24. package/web/dist/assets/ghostty-D-Js4rdm.png +0 -0
  25. package/web/dist/assets/index-CSEt-Qiy.js +66 -0
  26. package/web/dist/assets/index-RsXXnrVz.css +1 -0
  27. package/web/dist/assets/zed-C5BQT8X3.png +0 -0
  28. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  29. package/web/dist/icons/icon-192.png +0 -0
  30. package/web/dist/icons/icon-32.png +0 -0
  31. package/web/dist/icons/icon-512-maskable.png +0 -0
  32. package/web/dist/icons/icon-512.png +0 -0
  33. package/web/dist/index.html +11 -3
  34. package/web/dist/manifest.webmanifest +60 -0
  35. package/web/dist/screenshots/wide-overview.png +0 -0
  36. package/web/dist/sw.js +99 -0
  37. package/web/dist/assets/index-BHCSrZ_0.js +0 -66
  38. package/web/dist/assets/index-BUjVAMN8.css +0 -1
@@ -2,6 +2,21 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  export const PACKAGE_NAME = '@tt-a1i/hive';
5
+ /**
6
+ * Canonical argv for the upgrade command. Sharing one source between the
7
+ * server's install hint (`version-service.ts`) and the CLI upgrade path
8
+ * (`hive-update.ts`) keeps the two from drifting if the package name ever
9
+ * moves.
10
+ */
11
+ export const INSTALL_COMMAND_ARGS = ['install', '-g', `${PACKAGE_NAME}@latest`];
12
+ export const INSTALL_COMMAND_DISPLAY = `npm ${INSTALL_COMMAND_ARGS.join(' ')}`;
13
+ /**
14
+ * Windows ships npm as `npm.cmd` (a batch shim); Node's `child_process.spawn`
15
+ * will not resolve `.cmd` without `shell: true` or an explicit suffix, so the
16
+ * default `'npm'` produces ENOENT on Windows. Use this helper any time you
17
+ * spawn npm directly.
18
+ */
19
+ export const getNpmCommand = (platform = process.platform) => platform === 'win32' ? 'npm.cmd' : 'npm';
5
20
  export const readPackageVersion = () => {
6
21
  let dir = dirname(fileURLToPath(import.meta.url));
7
22
  for (let depth = 0; depth < 8; depth += 1) {
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import type { WorkerRole } from '../shared/types.js';
3
3
  import type { PickFolderResponse } from './fs-pick-folder.js';
4
+ import type { OpenCommandResult, OpenWorkspaceInput as OpenWorkspaceServiceInput } from './open-target-commands.js';
4
5
  import type { RuntimeStore } from './runtime-store.js';
5
6
  import type { TasksFileService } from './tasks-file.js';
6
7
  import type { VersionService } from './version-service.js';
@@ -48,12 +49,17 @@ export interface ConfigureAgentLaunchBody {
48
49
  args?: string[];
49
50
  command_preset_id?: string | null;
50
51
  }
52
+ export interface OpenWorkspaceBody {
53
+ target_id: string;
54
+ }
55
+ export type OpenWorkspaceService = (input: OpenWorkspaceServiceInput) => Promise<OpenCommandResult>;
51
56
  export interface RouteContext {
52
57
  request: IncomingMessage;
53
58
  response: ServerResponse;
54
59
  store: RuntimeStore;
55
60
  tasksFileService: TasksFileService;
56
61
  pickFolderService: () => Promise<PickFolderResponse>;
62
+ openWorkspaceService: OpenWorkspaceService;
57
63
  versionService: VersionService;
58
64
  params: Record<string, string>;
59
65
  }
@@ -0,0 +1,2 @@
1
+ import type { RouteDefinition } from './route-types.js';
2
+ export declare const openWorkspaceRoutes: RouteDefinition[];
@@ -0,0 +1,47 @@
1
+ import { isOpenTargetId } from './open-target-commands.js';
2
+ import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
3
+ import { requireUiTokenFromRequest } from './ui-auth-helpers.js';
4
+ export const openWorkspaceRoutes = [
5
+ route('POST', '/api/workspaces/:workspaceId/open', async ({ openWorkspaceService, params, request, response, store }) => {
6
+ const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
7
+ if (!workspaceId)
8
+ return;
9
+ requireUiTokenFromRequest(request, store.validateUiToken);
10
+ const body = await readJsonBody(request);
11
+ if (!isOpenTargetId(body.target_id)) {
12
+ sendJson(response, 400, { error: 'Unknown open target', target_id: body.target_id });
13
+ return;
14
+ }
15
+ // store.getWorkspaceSnapshot throws raw Error("Workspace not found: ...")
16
+ // for missing workspaces — translate that to 404 here rather than letting
17
+ // app.ts catch it as a generic 500.
18
+ let workspacePath;
19
+ try {
20
+ workspacePath = store.getWorkspaceSnapshot(workspaceId).summary.path;
21
+ }
22
+ catch {
23
+ sendJson(response, 404, { error: 'Workspace not found' });
24
+ return;
25
+ }
26
+ const result = await openWorkspaceService({
27
+ path: workspacePath,
28
+ targetId: body.target_id,
29
+ });
30
+ if (result.ok) {
31
+ sendJson(response, 200, {
32
+ ok: true,
33
+ effective_target_id: result.effectiveTargetId,
34
+ });
35
+ return;
36
+ }
37
+ // Drop stderr from the wire response — defense in depth. The frontend
38
+ // never renders raw stderr (toast text is localized via error_code), so
39
+ // there's no reason to ship the OS-level message to the browser where
40
+ // it would live in the devtools network log.
41
+ sendJson(response, 502, {
42
+ ok: false,
43
+ effective_target_id: result.effectiveTargetId,
44
+ error_code: result.errorCode,
45
+ });
46
+ }),
47
+ ];
@@ -1,6 +1,7 @@
1
1
  import { matchPath } from './route-helpers.js';
2
2
  import { dispatchRoutes } from './routes-dispatches.js';
3
3
  import { fsRoutes } from './routes-fs.js';
4
+ import { openWorkspaceRoutes } from './routes-open-workspace.js';
4
5
  import { runtimeRoutes } from './routes-runtime.js';
5
6
  import { settingsRoutes } from './routes-settings.js';
6
7
  import { taskRoutes } from './routes-tasks.js';
@@ -10,6 +11,7 @@ import { versionRoutes } from './routes-version.js';
10
11
  import { workspaceRoutes } from './routes-workspaces.js';
11
12
  const routes = [
12
13
  ...workspaceRoutes,
14
+ ...openWorkspaceRoutes,
13
15
  ...dispatchRoutes,
14
16
  ...versionRoutes,
15
17
  ...uiRoutes,
@@ -27,7 +27,10 @@ export const compareVersions = (left, right) => {
27
27
  };
28
28
  const buildVersionInfo = (currentVersion, latestVersion) => ({
29
29
  current_version: currentVersion,
30
- install_hint: `npm install -g ${PACKAGE_NAME}@latest`,
30
+ // Show `hive update` as the primary upgrade path now that the subcommand
31
+ // exists; it prints the npm fallback if its own spawn fails, so users
32
+ // running on Windows / Linux without a working npm in PATH still recover.
33
+ install_hint: 'hive update',
31
34
  latest_version: latestVersion,
32
35
  package_name: PACKAGE_NAME,
33
36
  release_url: `https://www.npmjs.com/package/${PACKAGE_NAME}/v/${latestVersion}`,
@@ -1,5 +1,7 @@
1
1
  import { basename } from 'node:path';
2
2
  const WORKSPACE_SHELL_SUFFIX = ':shell';
3
+ const WORKSPACE_SHELL_LABEL = 'Shell';
4
+ const EXITED_SHELL_RETENTION_MS = 5000;
3
5
  export const getWorkspaceShellAgentId = (workspaceId) => `${workspaceId}${WORKSPACE_SHELL_SUFFIX}`;
4
6
  export const isWorkspaceShellAgentId = (agentId) => agentId.endsWith(WORKSPACE_SHELL_SUFFIX);
5
7
  const shouldUseLoginShell = (command) => {
@@ -20,10 +22,10 @@ export const resolveWorkspaceShellLaunch = (env = process.env, platform = proces
20
22
  };
21
23
  export const createWorkspaceShellRuntime = (agentManager) => {
22
24
  const labelsByRunId = new Map();
23
- const nextShellNumberByWorkspaceId = new Map();
24
25
  const workspaceIdsByRunId = new Map();
25
26
  const runIdsByWorkspaceId = new Map();
26
27
  const startedAtByRunId = new Map();
28
+ const exitCleanupTimersByRunId = new Map();
27
29
  const requireManager = () => {
28
30
  if (!agentManager)
29
31
  throw new Error('Agent manager is required for workspace shell terminals');
@@ -43,6 +45,10 @@ export const createWorkspaceShellRuntime = (agentManager) => {
43
45
  }
44
46
  };
45
47
  const detachRun = (runId) => {
48
+ const exitCleanupTimer = exitCleanupTimersByRunId.get(runId);
49
+ if (exitCleanupTimer)
50
+ clearTimeout(exitCleanupTimer);
51
+ exitCleanupTimersByRunId.delete(runId);
46
52
  const workspaceId = workspaceIdsByRunId.get(runId);
47
53
  if (workspaceId) {
48
54
  const retained = (runIdsByWorkspaceId.get(workspaceId) ?? []).filter((id) => id !== runId);
@@ -61,11 +67,27 @@ export const createWorkspaceShellRuntime = (agentManager) => {
61
67
  startedAtByRunId.set(runId, startedAt);
62
68
  runIdsByWorkspaceId.set(workspaceId, [...(runIdsByWorkspaceId.get(workspaceId) ?? []), runId]);
63
69
  };
64
- const nextLabel = (workspaceId) => {
65
- const next = nextShellNumberByWorkspaceId.get(workspaceId) ?? 1;
66
- nextShellNumberByWorkspaceId.set(workspaceId, next + 1);
67
- return `Shell ${next}`;
70
+ const forgetShellRun = (runId) => {
71
+ detachRun(runId);
72
+ try {
73
+ requireManager().removeRun(runId);
74
+ }
75
+ catch {
76
+ // The PTY manager may have already dropped the run.
77
+ }
78
+ };
79
+ const handleShellExit = (runId) => {
80
+ if (!hasRun(runId) || exitCleanupTimersByRunId.has(runId))
81
+ return;
82
+ const timer = setTimeout(() => {
83
+ exitCleanupTimersByRunId.delete(runId);
84
+ if (hasRun(runId))
85
+ forgetShellRun(runId);
86
+ }, EXITED_SHELL_RETENTION_MS);
87
+ timer.unref?.();
88
+ exitCleanupTimersByRunId.set(runId, timer);
68
89
  };
90
+ const isListedRun = (run) => run.status === 'starting' || run.status === 'running';
69
91
  const stopPtyRun = (runId) => {
70
92
  requireManager().stopRun(runId);
71
93
  };
@@ -92,7 +114,9 @@ export const createWorkspaceShellRuntime = (agentManager) => {
92
114
  workspaceIdsByRunId.clear();
93
115
  startedAtByRunId.clear();
94
116
  labelsByRunId.clear();
95
- nextShellNumberByWorkspaceId.clear();
117
+ for (const timer of exitCleanupTimersByRunId.values())
118
+ clearTimeout(timer);
119
+ exitCleanupTimersByRunId.clear();
96
120
  },
97
121
  closeRun(workspaceId, runId) {
98
122
  if (workspaceIdsByRunId.get(runId) !== workspaceId)
@@ -110,7 +134,6 @@ export const createWorkspaceShellRuntime = (agentManager) => {
110
134
  }
111
135
  }
112
136
  runIdsByWorkspaceId.delete(workspaceId);
113
- nextShellNumberByWorkspaceId.delete(workspaceId);
114
137
  },
115
138
  getLiveRun(runId) {
116
139
  if (!hasRun(runId))
@@ -122,6 +145,8 @@ export const createWorkspaceShellRuntime = (agentManager) => {
122
145
  return (runIdsByWorkspaceId.get(workspaceId) ?? []).flatMap((runId) => {
123
146
  try {
124
147
  const run = toLiveRun(runId);
148
+ if (!isListedRun(run))
149
+ return [];
125
150
  return [
126
151
  {
127
152
  agent_id: getWorkspaceShellAgentId(workspaceId),
@@ -163,8 +188,9 @@ export const createWorkspaceShellRuntime = (agentManager) => {
163
188
  TERM: 'xterm-256color',
164
189
  TERM_PROGRAM: 'hive-shell',
165
190
  },
191
+ onExit: ({ runId }) => handleShellExit(runId),
166
192
  });
167
- attachRun(workspace.id, run.runId, nextLabel(workspace.id), startedAt);
193
+ attachRun(workspace.id, run.runId, WORKSPACE_SHELL_LABEL, startedAt);
168
194
  return { ...run, startedAt };
169
195
  },
170
196
  stopRun(runId) {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Cross-cutting types for the "Open workspace in editor/app" feature.
3
+ * Both the server (command construction in `src/server/open-target-commands.ts`)
4
+ * and the web client (button + preference store in `web/src/workspace/open-targets.ts`)
5
+ * pull the union and platform whitelist from here so they cannot drift.
6
+ */
7
+ export type OpenTargetId = 'vscode' | 'vscode-insiders' | 'cursor' | 'finder' | 'terminal' | 'ghostty' | 'zed';
8
+ export type OpenTargetPlatform = 'mac' | 'windows' | 'linux' | 'other';
9
+ export declare const OPEN_TARGET_IDS_BY_PLATFORM: Record<OpenTargetPlatform, readonly OpenTargetId[]>;
10
+ export declare const isOpenTargetId: (value: unknown) => value is OpenTargetId;
11
+ export declare const isOpenTargetSupported: (targetId: OpenTargetId, platform: OpenTargetPlatform) => boolean;
12
+ /**
13
+ * The id the server will actually attempt to launch. If the user's saved
14
+ * preference is unsupported on the current platform (e.g. they picked iTerm2
15
+ * on a Mac, then opened Hive on Windows), fall back to the platform default
16
+ * rather than erroring out — a stale preference shouldn't break the button.
17
+ */
18
+ export declare const getEffectiveOpenTargetId: (targetId: OpenTargetId, platform: OpenTargetPlatform) => OpenTargetId;
19
+ export declare const getDefaultOpenTargetIdForPlatform: (platform: OpenTargetPlatform) => OpenTargetId;
20
+ export type OpenWorkspaceErrorCode = 'invalid-path' | 'invalid-target' | 'app-not-installed' | 'command-not-in-path' | 'unknown';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Cross-cutting types for the "Open workspace in editor/app" feature.
3
+ * Both the server (command construction in `src/server/open-target-commands.ts`)
4
+ * and the web client (button + preference store in `web/src/workspace/open-targets.ts`)
5
+ * pull the union and platform whitelist from here so they cannot drift.
6
+ */
7
+ // Note: there is no `cursor-insiders` here. Cursor stopped shipping a separate
8
+ // Nightly bundle / `cursor-nightly` binary in March 2024; the pre-release
9
+ // channel is now an in-app toggle on the regular Cursor.app.
10
+ //
11
+ // IntelliJ IDEA, Windsurf, and iTerm2 were intentionally dropped after 1.3.0:
12
+ // IntelliJ users typically launch from JetBrains Toolbox rather than a folder
13
+ // picker; Windsurf overlaps with Cursor/VS Code; iTerm2 overlaps with the
14
+ // built-in macOS Terminal entry.
15
+ export const OPEN_TARGET_IDS_BY_PLATFORM = {
16
+ mac: ['vscode', 'vscode-insiders', 'cursor', 'finder', 'terminal', 'ghostty', 'zed'],
17
+ windows: ['vscode', 'vscode-insiders', 'cursor', 'finder', 'zed'],
18
+ linux: ['vscode', 'vscode-insiders', 'cursor', 'finder', 'zed'],
19
+ other: ['vscode', 'vscode-insiders', 'finder'],
20
+ };
21
+ const ALL_TARGET_IDS = new Set(OPEN_TARGET_IDS_BY_PLATFORM.mac);
22
+ export const isOpenTargetId = (value) => typeof value === 'string' && ALL_TARGET_IDS.has(value);
23
+ export const isOpenTargetSupported = (targetId, platform) => OPEN_TARGET_IDS_BY_PLATFORM[platform].includes(targetId);
24
+ /**
25
+ * The id the server will actually attempt to launch. If the user's saved
26
+ * preference is unsupported on the current platform (e.g. they picked iTerm2
27
+ * on a Mac, then opened Hive on Windows), fall back to the platform default
28
+ * rather than erroring out — a stale preference shouldn't break the button.
29
+ */
30
+ export const getEffectiveOpenTargetId = (targetId, platform) => isOpenTargetSupported(targetId, platform) ? targetId : getDefaultOpenTargetIdForPlatform(platform);
31
+ export const getDefaultOpenTargetIdForPlatform = (platform) => {
32
+ // `finder` exists for every platform and never fails closed.
33
+ if (platform === 'mac' || platform === 'windows' || platform === 'linux')
34
+ return 'finder';
35
+ return 'vscode';
36
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tt-a1i/hive",
3
- "version": "1.1.5",
3
+ "version": "1.3.0",
4
4
  "description": "Browser-native hive-mind for CLI coding agents — Claude Code, Codex, Gemini, and OpenCode collaborate as real PTY processes via a team protocol.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.30.3",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/tt-a1i/hive.git"
20
+ "url": "git+https://github.com/tt-a1i/hive-private.git"
21
21
  },
22
22
  "bugs": {
23
23
  "url": "https://github.com/tt-a1i/hive/issues"
@@ -60,7 +60,7 @@
60
60
  "postinstall": "node scripts/fix-runtime-artifacts.mjs",
61
61
  "release:dry": "pnpm check && pnpm build && pnpm test && pnpm pack:check && pnpm pack:smoke",
62
62
  "test": "vitest run",
63
- "test:windows": "vitest run tests/unit/agent-command-resolver.test.ts tests/unit/session-capture-multi-cli.test.ts tests/unit/claude-session-support.test.ts tests/unit/worker-name-generator.test.ts tests/server/fs-pick-folder.test.ts tests/server/fs-browse.test.ts tests/server/schema-version.test.ts tests/server/runtime-rehydration.test.ts tests/web/workspace-picker.test.tsx tests/web/confirm-dialog.test.tsx tests/web/toast.test.tsx --no-file-parallelism --maxWorkers=1 --testTimeout=30000 --hookTimeout=30000",
63
+ "test:windows": "vitest run tests/unit/agent-command-resolver.test.ts tests/unit/session-capture-multi-cli.test.ts tests/unit/claude-session-support.test.ts tests/unit/worker-name-generator.test.ts tests/unit/open-target-commands.test.ts tests/unit/sw-template-substitution.test.ts tests/unit/build-sw-plugin.test.ts tests/server/fs-pick-folder.test.ts tests/server/fs-browse.test.ts tests/server/schema-version.test.ts tests/server/runtime-rehydration.test.ts tests/server/open-workspace-route.test.ts tests/server/static-pwa.test.ts tests/web/workspace-picker.test.tsx tests/web/confirm-dialog.test.tsx tests/web/toast.test.tsx tests/web/open-workspace-button.test.tsx tests/web/register-service-worker.test.ts tests/web/use-shortcut-action.test.ts tests/web/update-available-toast.test.tsx tests/web/runtime-offline-page.test.tsx tests/web/use-terminal-panel-height.test.ts tests/web/use-terminal-panel-tabs.test.ts tests/web/terminal-tabs.test.tsx tests/web/terminal-bottom-panel.test.tsx --no-file-parallelism --maxWorkers=1 --testTimeout=60000 --hookTimeout=60000",
64
64
  "test:watch": "vitest"
65
65
  },
66
66
  "dependencies": {