whale-code 6.4.0 → 6.5.1
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/bin/swagmanager-mcp.js +51 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +65 -8
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +7 -6
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +85 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +46 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +36 -17
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +9 -6
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +25 -2
- package/dist/shared/agent-core.js +66 -5
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +15 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Config Store
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Unified auth session at ~/.whaletools/session.json
|
|
5
|
+
* User preferences at ~/.whaletools/preferences.json
|
|
5
6
|
*
|
|
6
7
|
* v2.0: Raw Supabase/Anthropic keys (for MCP server env vars)
|
|
7
8
|
* v2.1: Auth tokens from login flow (for CLI chat/status)
|
|
9
|
+
* v4.0: Shared auth with Swift apps via ~/.whaletools/session.json
|
|
8
10
|
*
|
|
9
11
|
* Environment variables always override file-based config for MCP server mode.
|
|
10
12
|
*/
|
|
@@ -14,32 +16,111 @@ import { join } from "path";
|
|
|
14
16
|
// ============================================================================
|
|
15
17
|
// PATHS
|
|
16
18
|
// ============================================================================
|
|
17
|
-
const CONFIG_DIR = join(homedir(), ".
|
|
18
|
-
const
|
|
19
|
+
const CONFIG_DIR = join(homedir(), ".whaletools");
|
|
20
|
+
const SESSION_PATH = join(CONFIG_DIR, "session.json");
|
|
21
|
+
const PREFS_PATH = join(CONFIG_DIR, "preferences.json");
|
|
22
|
+
// Legacy paths for migration
|
|
23
|
+
const LEGACY_CONFIG_DIR = join(homedir(), ".swagmanager");
|
|
24
|
+
const LEGACY_CONFIG_PATH = join(LEGACY_CONFIG_DIR, "config.json");
|
|
19
25
|
// ============================================================================
|
|
20
|
-
//
|
|
26
|
+
// AUTO-MIGRATION from ~/.swagmanager to ~/.whaletools
|
|
27
|
+
// ============================================================================
|
|
28
|
+
let migrationChecked = false;
|
|
29
|
+
function ensureMigration() {
|
|
30
|
+
if (migrationChecked)
|
|
31
|
+
return;
|
|
32
|
+
migrationChecked = true;
|
|
33
|
+
// Skip if new session already exists
|
|
34
|
+
if (existsSync(SESSION_PATH))
|
|
35
|
+
return;
|
|
36
|
+
// Check for legacy config
|
|
37
|
+
if (!existsSync(LEGACY_CONFIG_PATH))
|
|
38
|
+
return;
|
|
39
|
+
try {
|
|
40
|
+
const legacy = JSON.parse(readFileSync(LEGACY_CONFIG_PATH, "utf-8"));
|
|
41
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
42
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
43
|
+
}
|
|
44
|
+
// Split auth fields into session.json
|
|
45
|
+
if (legacy.access_token && legacy.refresh_token) {
|
|
46
|
+
const session = {
|
|
47
|
+
access_token: legacy.access_token,
|
|
48
|
+
refresh_token: legacy.refresh_token,
|
|
49
|
+
user_id: legacy.user_id,
|
|
50
|
+
email: legacy.email,
|
|
51
|
+
store_id: legacy.store_id,
|
|
52
|
+
store_name: legacy.store_name,
|
|
53
|
+
expires_at: legacy.expires_at,
|
|
54
|
+
};
|
|
55
|
+
writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + "\n", {
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
mode: 0o600,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Split preference fields into preferences.json
|
|
61
|
+
const prefs = {};
|
|
62
|
+
if (legacy.default_model)
|
|
63
|
+
prefs.default_model = legacy.default_model;
|
|
64
|
+
if (legacy.thinking_enabled !== undefined)
|
|
65
|
+
prefs.thinking_enabled = legacy.thinking_enabled;
|
|
66
|
+
if (legacy.permission_mode)
|
|
67
|
+
prefs.permission_mode = legacy.permission_mode;
|
|
68
|
+
if (legacy.platform_url)
|
|
69
|
+
prefs.platform_url = legacy.platform_url;
|
|
70
|
+
if (Object.keys(prefs).length > 0) {
|
|
71
|
+
writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2) + "\n", {
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
mode: 0o600,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Migration failed — not fatal, user can re-login
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// READ / WRITE — session.json (auth tokens + store)
|
|
21
83
|
// ============================================================================
|
|
22
84
|
export function loadConfig() {
|
|
85
|
+
ensureMigration();
|
|
23
86
|
try {
|
|
24
|
-
if (existsSync(
|
|
25
|
-
|
|
87
|
+
if (existsSync(SESSION_PATH)) {
|
|
88
|
+
const session = JSON.parse(readFileSync(SESSION_PATH, "utf-8"));
|
|
89
|
+
// Merge preferences so callers see a unified config
|
|
90
|
+
const prefs = loadPreferences();
|
|
91
|
+
return { ...prefs, ...session };
|
|
26
92
|
}
|
|
27
93
|
}
|
|
28
94
|
catch (err) {
|
|
29
|
-
|
|
30
|
-
console.error(`[config] Warning: Failed to parse ${CONFIG_PATH}: ${err instanceof Error ? err.message : err}`);
|
|
95
|
+
console.error(`[config] Warning: Failed to parse ${SESSION_PATH}: ${err instanceof Error ? err.message : err}`);
|
|
31
96
|
console.error("[config] Using empty config. You may need to re-login with: whale login");
|
|
32
97
|
}
|
|
33
|
-
return
|
|
98
|
+
// Even if no session, return preferences
|
|
99
|
+
return { ...loadPreferences() };
|
|
34
100
|
}
|
|
35
101
|
export function saveConfig(config) {
|
|
36
102
|
if (!existsSync(CONFIG_DIR)) {
|
|
37
103
|
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
38
104
|
}
|
|
39
|
-
|
|
105
|
+
// Split: auth fields go to session.json, prefs go to preferences.json
|
|
106
|
+
const { default_model, thinking_enabled, permission_mode, platform_url, ...authFields } = config;
|
|
107
|
+
writeFileSync(SESSION_PATH, JSON.stringify(authFields, null, 2) + "\n", {
|
|
40
108
|
encoding: "utf-8",
|
|
41
109
|
mode: 0o600,
|
|
42
110
|
});
|
|
111
|
+
// Update preferences if any pref fields are present
|
|
112
|
+
const newPrefs = {};
|
|
113
|
+
if (default_model !== undefined)
|
|
114
|
+
newPrefs.default_model = default_model;
|
|
115
|
+
if (thinking_enabled !== undefined)
|
|
116
|
+
newPrefs.thinking_enabled = thinking_enabled;
|
|
117
|
+
if (permission_mode !== undefined)
|
|
118
|
+
newPrefs.permission_mode = permission_mode;
|
|
119
|
+
if (platform_url !== undefined)
|
|
120
|
+
newPrefs.platform_url = platform_url;
|
|
121
|
+
if (Object.keys(newPrefs).length > 0) {
|
|
122
|
+
savePreferences({ ...loadPreferences(), ...newPrefs });
|
|
123
|
+
}
|
|
43
124
|
}
|
|
44
125
|
export function updateConfig(partial) {
|
|
45
126
|
const existing = loadConfig();
|
|
@@ -47,10 +128,32 @@ export function updateConfig(partial) {
|
|
|
47
128
|
}
|
|
48
129
|
export function clearConfig() {
|
|
49
130
|
try {
|
|
50
|
-
if (existsSync(
|
|
51
|
-
unlinkSync(
|
|
131
|
+
if (existsSync(SESSION_PATH))
|
|
132
|
+
unlinkSync(SESSION_PATH);
|
|
133
|
+
}
|
|
134
|
+
catch { /* ignore */ }
|
|
135
|
+
// Preferences are preserved across sign-out
|
|
136
|
+
}
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// READ / WRITE — preferences.json (survives sign-out)
|
|
139
|
+
// ============================================================================
|
|
140
|
+
export function loadPreferences() {
|
|
141
|
+
try {
|
|
142
|
+
if (existsSync(PREFS_PATH)) {
|
|
143
|
+
return JSON.parse(readFileSync(PREFS_PATH, "utf-8"));
|
|
144
|
+
}
|
|
52
145
|
}
|
|
53
146
|
catch { /* ignore */ }
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
export function savePreferences(prefs) {
|
|
150
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
151
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
152
|
+
}
|
|
153
|
+
writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2) + "\n", {
|
|
154
|
+
encoding: "utf-8",
|
|
155
|
+
mode: 0o600,
|
|
156
|
+
});
|
|
54
157
|
}
|
|
55
158
|
/** Default Fly.io agent server URL */
|
|
56
159
|
export const WHALE_SERVER_URL = "https://whale-agent.fly.dev";
|
|
@@ -71,7 +174,7 @@ export function resolveConfig() {
|
|
|
71
174
|
};
|
|
72
175
|
}
|
|
73
176
|
export function getConfigPath() {
|
|
74
|
-
return
|
|
177
|
+
return SESSION_PATH;
|
|
75
178
|
}
|
|
76
179
|
/** Lazy proxy URL — avoids reading config at import time */
|
|
77
180
|
export function getProxyUrl() {
|
|
@@ -18,6 +18,10 @@ const PRIORITY_COLUMNS = {
|
|
|
18
18
|
supply_chain: ["name", "status", "quantity", "supplier", "location", "expected_delivery_date"],
|
|
19
19
|
workflows: ["name", "status", "trigger_type", "created_at", "last_run"],
|
|
20
20
|
email: ["subject", "to", "status", "sent_at", "category"],
|
|
21
|
+
locations: ["name", "city", "state", "type", "is_active", "address_line1"],
|
|
22
|
+
media: ["title", "file_name", "category", "tags", "file_type"],
|
|
23
|
+
creations: ["name", "creation_type", "status", "visibility", "slug"],
|
|
24
|
+
api_keys: ["name", "key_type", "is_active", "scopes"],
|
|
21
25
|
};
|
|
22
26
|
// Keys that are always deprioritized (shown last or hidden)
|
|
23
27
|
const LOW_PRIORITY_KEYS = new Set([
|
|
@@ -27,9 +31,6 @@ const LOW_PRIORITY_KEYS = new Set([
|
|
|
27
31
|
// ============================================================================
|
|
28
32
|
// FORMATTERS
|
|
29
33
|
// ============================================================================
|
|
30
|
-
function isNarrow() {
|
|
31
|
-
return (process.stdout.columns || 80) < 90;
|
|
32
|
-
}
|
|
33
34
|
function truncateUuid(val) {
|
|
34
35
|
// UUID pattern: 8-4-4-4-12 hex chars
|
|
35
36
|
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val)) {
|
|
@@ -149,7 +150,8 @@ function selectColumns(rows, toolName) {
|
|
|
149
150
|
allKeys.add(key);
|
|
150
151
|
}
|
|
151
152
|
// Find matching priority list
|
|
152
|
-
const
|
|
153
|
+
const termWidth = process.stdout.columns || 80;
|
|
154
|
+
const maxCols = termWidth < 90 ? 4 : termWidth < 120 ? 6 : termWidth < 160 ? 8 : 10;
|
|
153
155
|
let priority = [];
|
|
154
156
|
if (toolName) {
|
|
155
157
|
// Match tool name to category
|
|
@@ -193,14 +195,18 @@ function buildTable(rows, columns) {
|
|
|
193
195
|
if (columns.length === 0 || rows.length === 0)
|
|
194
196
|
return "";
|
|
195
197
|
const headers = columns.map(prettifyKey);
|
|
196
|
-
// Calculate column widths
|
|
198
|
+
// Calculate column widths — dynamic cap based on terminal width and column count
|
|
199
|
+
const termW = process.stdout.columns || 80;
|
|
200
|
+
// Each column gets a fair share of available space (minus separators + borders)
|
|
201
|
+
const overhead = columns.length * 3 + 4; // " | " between cols + "| " and " |"
|
|
202
|
+
const cellCap = Math.min(Math.max(Math.floor((termW - overhead) / columns.length), 15), 50);
|
|
197
203
|
const widths = columns.map((col, i) => {
|
|
198
204
|
let max = headers[i].length;
|
|
199
205
|
for (const row of rows) {
|
|
200
206
|
const val = isMoneyKey(col) ? formatMoneyValue(row[col]) : formatValue(row[col], col);
|
|
201
207
|
max = Math.max(max, val.length);
|
|
202
208
|
}
|
|
203
|
-
return Math.min(max,
|
|
209
|
+
return Math.min(max, cellCap);
|
|
204
210
|
});
|
|
205
211
|
// Header row
|
|
206
212
|
const headerRow = columns.map((_, i) => headers[i].padEnd(widths[i])).join(" | ");
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink Resize Ghost Fix
|
|
3
|
+
*
|
|
4
|
+
* Patches Ink 6's resize handler to prevent ghost artifacts from text reflow.
|
|
5
|
+
*
|
|
6
|
+
* Problem: Ink's resized() clears the dynamic area using eraseLines(N) where N
|
|
7
|
+
* is the string line count. But when the terminal shrinks, already-painted text
|
|
8
|
+
* reflows to more visual lines than N, so eraseLines(N) doesn't erase enough,
|
|
9
|
+
* leaving "ghost" lines above the new render.
|
|
10
|
+
*
|
|
11
|
+
* Fix: After Ink's standard clear, calculate the extra visual lines caused by
|
|
12
|
+
* reflow at the new width and erase them with cursor-up + erase-to-end-of-screen.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Patches the Ink instance's resize handler to account for visual line reflow.
|
|
16
|
+
* Call this AFTER render() — the Ink instance must exist in the registry.
|
|
17
|
+
*/
|
|
18
|
+
export declare function patchInkResize(): Promise<void>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink Resize Ghost Fix
|
|
3
|
+
*
|
|
4
|
+
* Patches Ink 6's resize handler to prevent ghost artifacts from text reflow.
|
|
5
|
+
*
|
|
6
|
+
* Problem: Ink's resized() clears the dynamic area using eraseLines(N) where N
|
|
7
|
+
* is the string line count. But when the terminal shrinks, already-painted text
|
|
8
|
+
* reflows to more visual lines than N, so eraseLines(N) doesn't erase enough,
|
|
9
|
+
* leaving "ghost" lines above the new render.
|
|
10
|
+
*
|
|
11
|
+
* Fix: After Ink's standard clear, calculate the extra visual lines caused by
|
|
12
|
+
* reflow at the new width and erase them with cursor-up + erase-to-end-of-screen.
|
|
13
|
+
*/
|
|
14
|
+
import { pathToFileURL } from "url";
|
|
15
|
+
import { resolve, dirname } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
/**
|
|
18
|
+
* Patches the Ink instance's resize handler to account for visual line reflow.
|
|
19
|
+
* Call this AFTER render() — the Ink instance must exist in the registry.
|
|
20
|
+
*/
|
|
21
|
+
export async function patchInkResize() {
|
|
22
|
+
try {
|
|
23
|
+
// Dynamically import Ink's internal instance registry and wrap-ansi
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const instancesPath = pathToFileURL(resolve(__dirname, "..", "..", "..", "node_modules", "ink", "build", "instances.js")).href;
|
|
26
|
+
const [{ default: instances }, { default: wrapAnsi }] = await Promise.all([
|
|
27
|
+
import(/* @vite-ignore */ instancesPath),
|
|
28
|
+
import("wrap-ansi"),
|
|
29
|
+
]);
|
|
30
|
+
const ink = instances.get(process.stdout);
|
|
31
|
+
if (!ink)
|
|
32
|
+
return;
|
|
33
|
+
// Replace Ink's resized handler with our visual-line-aware version
|
|
34
|
+
ink.resized = () => {
|
|
35
|
+
const currentWidth = ink.getTerminalWidth();
|
|
36
|
+
if (currentWidth < ink.lastTerminalWidth) {
|
|
37
|
+
// Calculate how many visual lines the old output occupies at the NEW width.
|
|
38
|
+
// wrapAnsi handles ANSI escape codes, wide chars (CJK), and emoji correctly.
|
|
39
|
+
const oldOutput = ink.lastOutput || "";
|
|
40
|
+
const stringLineCount = oldOutput === "" ? 0 : oldOutput.split("\n").length;
|
|
41
|
+
let visualLineCount = stringLineCount;
|
|
42
|
+
if (oldOutput && currentWidth > 0) {
|
|
43
|
+
const wrapped = wrapAnsi(oldOutput, currentWidth, { trim: false, hard: true });
|
|
44
|
+
visualLineCount = wrapped === "" ? 0 : wrapped.split("\n").length;
|
|
45
|
+
}
|
|
46
|
+
const extraLines = Math.max(0, visualLineCount - stringLineCount);
|
|
47
|
+
// Standard clear — erases stringLineCount lines, resets log's internal state
|
|
48
|
+
ink.log.clear();
|
|
49
|
+
// Erase additional ghost lines caused by text reflow.
|
|
50
|
+
// After log.clear(), cursor is stringLineCount lines above bottom.
|
|
51
|
+
// Ghost lines are above that — move up extraLines more and erase to end of screen.
|
|
52
|
+
if (extraLines > 0) {
|
|
53
|
+
ink.options.stdout.write(`\x1b[${extraLines}A\x1b[J`);
|
|
54
|
+
}
|
|
55
|
+
ink.lastOutput = "";
|
|
56
|
+
ink.lastOutputToRender = "";
|
|
57
|
+
}
|
|
58
|
+
ink.calculateLayout();
|
|
59
|
+
ink.onRender();
|
|
60
|
+
ink.lastTerminalWidth = currentWidth;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Silent fail — resize fix is best-effort, app still works without it
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -27,6 +27,10 @@ export interface PlanModeState {
|
|
|
27
27
|
planContent?: string;
|
|
28
28
|
startedAt?: Date;
|
|
29
29
|
}
|
|
30
|
+
export interface PlanApprovalDecision {
|
|
31
|
+
action: "execute" | "edit" | "cancel" | "feedback";
|
|
32
|
+
feedback?: string;
|
|
33
|
+
}
|
|
30
34
|
export declare const interactiveEvents: EventEmitter<[never]>;
|
|
31
35
|
export declare function createQuestionRequest(questions: Question[]): QuestionRequest;
|
|
32
36
|
export declare function getPendingQuestion(): QuestionRequest | undefined;
|
|
@@ -40,8 +44,18 @@ export declare function exitPlanMode(): {
|
|
|
40
44
|
success: boolean;
|
|
41
45
|
message: string;
|
|
42
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Wait for user to approve/reject the plan via the UI overlay.
|
|
49
|
+
* Called by the agent loop after exitPlanMode() returns.
|
|
50
|
+
*/
|
|
51
|
+
export declare function waitForPlanApproval(): Promise<PlanApprovalDecision>;
|
|
43
52
|
export declare function isPlanMode(): boolean;
|
|
44
53
|
export declare function getPlanModeState(): PlanModeState;
|
|
54
|
+
export declare function getPendingPlanApproval(): {
|
|
55
|
+
planContent: string;
|
|
56
|
+
planFile: string;
|
|
57
|
+
} | null;
|
|
58
|
+
export declare function resolvePlanApproval(decision: PlanApprovalDecision): boolean;
|
|
45
59
|
export declare const INTERACTIVE_TOOL_DEFINITIONS: ({
|
|
46
60
|
name: string;
|
|
47
61
|
description: string;
|
|
@@ -14,6 +14,8 @@ import { resolve } from "path";
|
|
|
14
14
|
const pendingQuestions = new Map();
|
|
15
15
|
// Plan mode state
|
|
16
16
|
let planModeState = { active: false };
|
|
17
|
+
// Plan approval state — blocks exit_plan_mode until UI resolves
|
|
18
|
+
let pendingPlanApproval = null;
|
|
17
19
|
// Event emitter for UI coordination
|
|
18
20
|
export const interactiveEvents = new EventEmitter();
|
|
19
21
|
// ============================================================================
|
|
@@ -83,8 +85,7 @@ export function exitPlanMode() {
|
|
|
83
85
|
}
|
|
84
86
|
const planFile = planModeState.planFile || ".whale/plan.md";
|
|
85
87
|
planModeState = { active: false };
|
|
86
|
-
|
|
87
|
-
// Read the plan file to display its content
|
|
88
|
+
// Read the plan file content
|
|
88
89
|
const fullPath = resolve(process.cwd(), planFile);
|
|
89
90
|
let planContent = "";
|
|
90
91
|
if (existsSync(fullPath)) {
|
|
@@ -93,6 +94,15 @@ export function exitPlanMode() {
|
|
|
93
94
|
}
|
|
94
95
|
catch { /* ignore read errors */ }
|
|
95
96
|
}
|
|
97
|
+
// Emit event for UI to show approval overlay
|
|
98
|
+
interactiveEvents.emit("planModeExited", { planFile, planContent });
|
|
99
|
+
// Set up pending approval state so UI can resolve it via resolvePlanApproval()
|
|
100
|
+
pendingPlanApproval = {
|
|
101
|
+
resolve: () => { },
|
|
102
|
+
reject: () => { },
|
|
103
|
+
planContent,
|
|
104
|
+
planFile,
|
|
105
|
+
};
|
|
96
106
|
if (planContent) {
|
|
97
107
|
return {
|
|
98
108
|
success: true,
|
|
@@ -104,6 +114,25 @@ export function exitPlanMode() {
|
|
|
104
114
|
message: `Plan mode complete. Plan saved to ${planFile}.`,
|
|
105
115
|
};
|
|
106
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Wait for user to approve/reject the plan via the UI overlay.
|
|
119
|
+
* Called by the agent loop after exitPlanMode() returns.
|
|
120
|
+
*/
|
|
121
|
+
export async function waitForPlanApproval() {
|
|
122
|
+
if (!pendingPlanApproval) {
|
|
123
|
+
return { action: "execute" };
|
|
124
|
+
}
|
|
125
|
+
const { planContent, planFile } = pendingPlanApproval;
|
|
126
|
+
const decision = await new Promise((resolvePromise, rejectPromise) => {
|
|
127
|
+
pendingPlanApproval = {
|
|
128
|
+
resolve: resolvePromise,
|
|
129
|
+
reject: rejectPromise,
|
|
130
|
+
planContent,
|
|
131
|
+
planFile,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
return decision;
|
|
135
|
+
}
|
|
107
136
|
export function isPlanMode() {
|
|
108
137
|
return planModeState.active;
|
|
109
138
|
}
|
|
@@ -111,6 +140,22 @@ export function getPlanModeState() {
|
|
|
111
140
|
return { ...planModeState };
|
|
112
141
|
}
|
|
113
142
|
// ============================================================================
|
|
143
|
+
// PLAN APPROVAL — blocking resolution from UI
|
|
144
|
+
// ============================================================================
|
|
145
|
+
export function getPendingPlanApproval() {
|
|
146
|
+
if (!pendingPlanApproval)
|
|
147
|
+
return null;
|
|
148
|
+
return { planContent: pendingPlanApproval.planContent, planFile: pendingPlanApproval.planFile };
|
|
149
|
+
}
|
|
150
|
+
export function resolvePlanApproval(decision) {
|
|
151
|
+
if (!pendingPlanApproval)
|
|
152
|
+
return false;
|
|
153
|
+
const { resolve } = pendingPlanApproval;
|
|
154
|
+
pendingPlanApproval = null;
|
|
155
|
+
resolve(decision);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
// ============================================================================
|
|
114
159
|
// TOOL DEFINITIONS
|
|
115
160
|
// ============================================================================
|
|
116
161
|
export const INTERACTIVE_TOOL_DEFINITIONS = [
|
|
@@ -12,7 +12,7 @@ import { homedir } from "os";
|
|
|
12
12
|
// ============================================================================
|
|
13
13
|
const DEFAULTS = {
|
|
14
14
|
cancel_stream: "escape",
|
|
15
|
-
toggle_expand: "ctrl+
|
|
15
|
+
toggle_expand: "ctrl+o",
|
|
16
16
|
toggle_thinking: "ctrl+t",
|
|
17
17
|
exit: "ctrl+c",
|
|
18
18
|
clear_line: "ctrl+u",
|
|
@@ -14,7 +14,7 @@ import { globSearch, grepSearch } from "./tools/search-tools.js";
|
|
|
14
14
|
import { runCommand, bashOutput, killShell, listShellsFn } from "./tools/shell-exec.js";
|
|
15
15
|
import { webFetch, webSearch } from "./tools/web-tools.js";
|
|
16
16
|
import { tasksTool } from "./tools/task-manager.js";
|
|
17
|
-
import { taskTool, teamCreateTool, taskOutput, taskStop, configTool, askUser, lspTool, skillTool } from "./tools/agent-tools.js";
|
|
17
|
+
import { taskTool, teamCreateTool, teamAutoTool, taskOutput, taskStop, configTool, askUser, lspTool, skillTool } from "./tools/agent-tools.js";
|
|
18
18
|
// ============================================================================
|
|
19
19
|
// TOOL NAMES
|
|
20
20
|
// ============================================================================
|
|
@@ -36,7 +36,8 @@ export const LOCAL_TOOL_NAMES = new Set([
|
|
|
36
36
|
"tasks", // Replaces todo_write — action-based CRUD with IDs, deps
|
|
37
37
|
"multi_edit", // Multi-edit tool
|
|
38
38
|
"task", // Subagent tool
|
|
39
|
-
"team_create", // Agent team tool
|
|
39
|
+
"team_create", // Agent team tool (explicit tasks)
|
|
40
|
+
"team_auto", // Auto-decompose + parallel team + review
|
|
40
41
|
// Background process tools
|
|
41
42
|
"bash_output",
|
|
42
43
|
"kill_shell",
|
|
@@ -421,6 +422,37 @@ export const LOCAL_TOOL_DEFINITIONS = [
|
|
|
421
422
|
required: ["name", "teammate_count", "tasks"],
|
|
422
423
|
},
|
|
423
424
|
},
|
|
425
|
+
{
|
|
426
|
+
name: "team_auto",
|
|
427
|
+
description: "Auto-decompose a task and run it as a parallel agent team. AI breaks the task into sub-tasks with file ownership, spawns teammates, executes in parallel, and reviews results. Use this for large tasks where you don't want to manually plan the decomposition.",
|
|
428
|
+
input_schema: {
|
|
429
|
+
type: "object",
|
|
430
|
+
properties: {
|
|
431
|
+
task: {
|
|
432
|
+
type: "string",
|
|
433
|
+
description: "The task to decompose and execute (e.g., 'Refactor all components to use TypeScript strict mode')",
|
|
434
|
+
},
|
|
435
|
+
max_teammates: {
|
|
436
|
+
type: "number",
|
|
437
|
+
description: "Maximum number of parallel teammates (default: 4, max: 6)",
|
|
438
|
+
},
|
|
439
|
+
model: {
|
|
440
|
+
type: "string",
|
|
441
|
+
enum: ["sonnet", "opus", "haiku"],
|
|
442
|
+
description: "Model for teammates (default: sonnet)",
|
|
443
|
+
},
|
|
444
|
+
working_directory: {
|
|
445
|
+
type: "string",
|
|
446
|
+
description: "Project directory to work in (default: cwd)",
|
|
447
|
+
},
|
|
448
|
+
review: {
|
|
449
|
+
type: "boolean",
|
|
450
|
+
description: "Run a review pass after completion (default: true)",
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
required: ["task"],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
424
456
|
// ------------------------------------------------------------------
|
|
425
457
|
// BACKGROUND PROCESS TOOLS
|
|
426
458
|
// ------------------------------------------------------------------
|
|
@@ -682,6 +714,7 @@ export async function executeLocalTool(name, input) {
|
|
|
682
714
|
// Agent tools (tools/agent-tools.ts)
|
|
683
715
|
case "task": return await taskTool(input);
|
|
684
716
|
case "team_create": return await teamCreateTool(input);
|
|
717
|
+
case "team_auto": return await teamAutoTool(input);
|
|
685
718
|
case "task_output": return await taskOutput(input);
|
|
686
719
|
case "task_stop": return taskStop(input);
|
|
687
720
|
case "config": return configTool(input);
|