helloloop 0.8.6 → 0.10.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 (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +230 -498
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/gemini/extension/gemini-extension.json +1 -1
  6. package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
  7. package/native/windows-hidden-shell-proxy/Program.cs +498 -0
  8. package/package.json +4 -2
  9. package/src/activity_projection.mjs +294 -0
  10. package/src/analyze_confirmation.mjs +3 -1
  11. package/src/analyzer.mjs +2 -1
  12. package/src/auto_execution_options.mjs +13 -0
  13. package/src/background_launch.mjs +73 -0
  14. package/src/cli.mjs +51 -1
  15. package/src/cli_analyze_command.mjs +12 -14
  16. package/src/cli_args.mjs +106 -32
  17. package/src/cli_command_handlers.mjs +73 -25
  18. package/src/cli_support.mjs +2 -0
  19. package/src/common.mjs +11 -0
  20. package/src/dashboard_command.mjs +371 -0
  21. package/src/dashboard_tui.mjs +289 -0
  22. package/src/dashboard_web.mjs +351 -0
  23. package/src/dashboard_web_client.mjs +167 -0
  24. package/src/dashboard_web_page.mjs +49 -0
  25. package/src/engine_event_parser_codex.mjs +167 -0
  26. package/src/engine_process_support.mjs +7 -2
  27. package/src/engine_selection.mjs +24 -0
  28. package/src/engine_selection_probe.mjs +10 -6
  29. package/src/engine_selection_settings.mjs +53 -44
  30. package/src/execution_interactivity.mjs +12 -0
  31. package/src/host_continuation.mjs +305 -0
  32. package/src/install_codex.mjs +20 -30
  33. package/src/install_shared.mjs +9 -0
  34. package/src/node_process_launch.mjs +28 -0
  35. package/src/process.mjs +2 -0
  36. package/src/runner_execute_task.mjs +15 -1
  37. package/src/runner_execution_support.mjs +69 -3
  38. package/src/runner_once.mjs +5 -0
  39. package/src/runner_status.mjs +72 -4
  40. package/src/runtime_engine_support.mjs +52 -5
  41. package/src/runtime_engine_task.mjs +7 -0
  42. package/src/runtime_settings.mjs +105 -0
  43. package/src/runtime_settings_loader.mjs +19 -0
  44. package/src/shell_invocation.mjs +227 -9
  45. package/src/supervisor_cli_support.mjs +49 -0
  46. package/src/supervisor_guardian.mjs +307 -0
  47. package/src/supervisor_runtime.mjs +142 -83
  48. package/src/supervisor_state.mjs +64 -0
  49. package/src/supervisor_watch.mjs +364 -0
  50. package/src/terminal_session_limits.mjs +1 -21
  51. package/src/windows_hidden_shell_proxy.mjs +405 -0
  52. package/src/workspace_registry.mjs +155 -0
@@ -0,0 +1,167 @@
1
+ function normalizeWhitespace(value) {
2
+ return String(value || "").replace(/\s+/gu, " ").trim();
3
+ }
4
+
5
+ function stripMarkdown(value) {
6
+ return normalizeWhitespace(String(value || "")
7
+ .replace(/\*\*(.*?)\*\*/gu, "$1")
8
+ .replace(/`([^`]+)`/gu, "$1")
9
+ .replace(/^#+\s*/gmu, ""));
10
+ }
11
+
12
+ function shorten(value, maxLength = 160) {
13
+ const text = normalizeWhitespace(value);
14
+ if (!text) {
15
+ return "";
16
+ }
17
+ return text.length <= maxLength ? text : `${text.slice(0, Math.max(0, maxLength - 1))}…`;
18
+ }
19
+
20
+ function summarizeText(value, maxLength = 160) {
21
+ const sections = String(value || "")
22
+ .replace(/\r\n/gu, "\n")
23
+ .split(/\n\s*\n/gu)
24
+ .map((item) => stripMarkdown(item))
25
+ .filter(Boolean);
26
+ return shorten(sections[0] || stripMarkdown(value), maxLength);
27
+ }
28
+
29
+ function normalizeExitCode(value) {
30
+ const code = Number(value);
31
+ return Number.isFinite(code) ? code : null;
32
+ }
33
+
34
+ function normalizeTodoItems(items) {
35
+ const normalized = Array.isArray(items)
36
+ ? items.map((item) => ({
37
+ text: shorten(item?.text || "", 140),
38
+ completed: item?.completed === true,
39
+ }))
40
+ : [];
41
+ const completed = normalized.filter((item) => item.completed).length;
42
+ return {
43
+ items: normalized,
44
+ total: normalized.length,
45
+ completed,
46
+ pending: Math.max(0, normalized.length - completed),
47
+ };
48
+ }
49
+
50
+ function normalizeChanges(changes) {
51
+ return Array.isArray(changes)
52
+ ? changes.map((change) => ({
53
+ path: String(change?.path || "").trim(),
54
+ kind: String(change?.kind || "update").trim() || "update",
55
+ }))
56
+ : [];
57
+ }
58
+
59
+ function buildFileChangeLabel(changes) {
60
+ if (!changes.length) {
61
+ return "文件变更";
62
+ }
63
+ if (changes.length === 1) {
64
+ const change = changes[0];
65
+ return `${change.kind} ${shorten(change.path, 100)}`;
66
+ }
67
+ return `${changes.length} 个文件变更`;
68
+ }
69
+
70
+ export function parseCodexJsonlEventLine(line) {
71
+ const payload = JSON.parse(line);
72
+ const type = String(payload?.type || "").trim();
73
+
74
+ if (type === "thread.started") {
75
+ return {
76
+ kind: "thread",
77
+ status: "started",
78
+ label: "线程已启动",
79
+ threadId: String(payload.thread_id || "").trim(),
80
+ };
81
+ }
82
+
83
+ if (type === "turn.started") {
84
+ return {
85
+ kind: "turn",
86
+ status: "started",
87
+ label: "轮次开始",
88
+ };
89
+ }
90
+
91
+ if (type === "turn.completed") {
92
+ return {
93
+ kind: "turn",
94
+ status: "completed",
95
+ label: "轮次完成",
96
+ };
97
+ }
98
+
99
+ if (!type.startsWith("item.")) {
100
+ return {
101
+ kind: "event",
102
+ status: "info",
103
+ label: shorten(type, 120) || "事件更新",
104
+ };
105
+ }
106
+
107
+ const item = payload.item || {};
108
+ const itemId = String(item.id || "").trim();
109
+ const itemStatus = type === "item.started"
110
+ ? "in_progress"
111
+ : (String(item.status || "").trim() || "completed");
112
+
113
+ if (item.type === "reasoning") {
114
+ const summary = summarizeText(item.text || "", 180);
115
+ return {
116
+ kind: "reasoning",
117
+ itemId,
118
+ status: itemStatus,
119
+ label: summary || "推理更新",
120
+ summary,
121
+ };
122
+ }
123
+
124
+ if (item.type === "todo_list") {
125
+ const todo = normalizeTodoItems(item.items);
126
+ return {
127
+ kind: "todo",
128
+ itemId,
129
+ status: itemStatus,
130
+ label: todo.total > 0
131
+ ? `待办 ${todo.completed}/${todo.total}`
132
+ : "待办清单更新",
133
+ todo,
134
+ };
135
+ }
136
+
137
+ if (item.type === "command_execution") {
138
+ const command = shorten(item.command || "", 220);
139
+ return {
140
+ kind: "command",
141
+ itemId,
142
+ status: itemStatus,
143
+ label: command || "命令执行",
144
+ command,
145
+ exitCode: normalizeExitCode(item.exit_code),
146
+ outputSummary: summarizeText(item.aggregated_output || "", 160),
147
+ };
148
+ }
149
+
150
+ if (item.type === "file_change") {
151
+ const changes = normalizeChanges(item.changes);
152
+ return {
153
+ kind: "file_change",
154
+ itemId,
155
+ status: itemStatus,
156
+ label: buildFileChangeLabel(changes),
157
+ changes,
158
+ };
159
+ }
160
+
161
+ return {
162
+ kind: String(item.type || "item").trim() || "item",
163
+ itemId,
164
+ status: itemStatus,
165
+ label: summarizeText(item.text || item.command || item.type || type, 160) || "活动更新",
166
+ };
167
+ }
@@ -26,6 +26,7 @@ export function runChild(command, args, options = {}) {
26
26
  },
27
27
  stdio: ["pipe", "pipe", "pipe"],
28
28
  shell: Boolean(options.shell),
29
+ windowsHide: process.platform === "win32",
29
30
  });
30
31
 
31
32
  let stdout = "";
@@ -121,17 +122,21 @@ export function runChild(command, args, options = {}) {
121
122
  emitHeartbeat("running");
122
123
 
123
124
  child.stdout.on("data", (chunk) => {
124
- stdout += chunk.toString();
125
+ const text = chunk.toString();
126
+ stdout += text;
125
127
  stdoutBytes += chunk.length;
126
128
  lastOutputAt = Date.now();
127
129
  stallWarned = false;
130
+ options.onStdout?.(text);
128
131
  emitHeartbeat("running");
129
132
  });
130
133
  child.stderr.on("data", (chunk) => {
131
- stderr += chunk.toString();
134
+ const text = chunk.toString();
135
+ stderr += text;
132
136
  stderrBytes += chunk.length;
133
137
  lastOutputAt = Date.now();
134
138
  stallWarned = false;
139
+ options.onStderr?.(text);
135
140
  emitHeartbeat("running");
136
141
  });
137
142
 
@@ -273,6 +273,30 @@ export async function resolveEngineSelection({
273
273
  };
274
274
  }
275
275
 
276
+ const recommendedEngine = recommendEngine({
277
+ hostContext,
278
+ availableEngines,
279
+ projectConfig,
280
+ userSettings,
281
+ });
282
+ if (!interactive && recommendedEngine) {
283
+ return buildResolution({
284
+ engine: recommendedEngine,
285
+ source: "remembered_default",
286
+ basis: [
287
+ buildRecommendationBasis({
288
+ hostContext,
289
+ projectConfig,
290
+ userSettings,
291
+ recommendedEngine,
292
+ }) || `继续沿用已确认过的默认执行引擎:${getEngineDisplayName(recommendedEngine)}。`,
293
+ "当前为无人值守续跑场景,HelloLoop 直接复用之前确认过的可用引擎。",
294
+ ],
295
+ hostContext,
296
+ probes,
297
+ });
298
+ }
299
+
276
300
  if (!interactive) {
277
301
  return {
278
302
  ok: false,
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
 
3
3
  import { getEngineDisplayName, getEngineMetadata, listKnownEngines, normalizeEngineName } from "./engine_metadata.mjs";
4
- import { resolveCliInvocation } from "./shell_invocation.mjs";
4
+ import { resolveCliInvocation, resolveCodexInvocation } from "./shell_invocation.mjs";
5
5
 
6
6
  function resolveExecutableOverride(policy = {}, engine) {
7
7
  const envExecutable = String(process.env[`HELLOLOOP_${String(engine || "").toUpperCase()}_EXECUTABLE`] || "").trim();
@@ -13,11 +13,14 @@ function resolveExecutableOverride(policy = {}, engine) {
13
13
 
14
14
  function probeEngineAvailability(engine, policy = {}) {
15
15
  const meta = getEngineMetadata(engine);
16
- const invocation = resolveCliInvocation({
17
- commandName: meta.commandName,
18
- toolDisplayName: meta.displayName,
19
- explicitExecutable: resolveExecutableOverride(policy, engine),
20
- });
16
+ const explicitExecutable = resolveExecutableOverride(policy, engine);
17
+ const invocation = engine === "codex"
18
+ ? resolveCodexInvocation({ explicitExecutable })
19
+ : resolveCliInvocation({
20
+ commandName: meta.commandName,
21
+ toolDisplayName: meta.displayName,
22
+ explicitExecutable,
23
+ });
21
24
 
22
25
  if (invocation.error) {
23
26
  return {
@@ -30,6 +33,7 @@ function probeEngineAvailability(engine, policy = {}) {
30
33
  const result = spawnSync(invocation.command, [...invocation.argsPrefix, "--version"], {
31
34
  encoding: "utf8",
32
35
  shell: invocation.shell,
36
+ windowsHide: process.platform === "win32",
33
37
  });
34
38
  const ok = result.status === 0;
35
39
  return {
@@ -3,6 +3,14 @@ import path from "node:path";
3
3
 
4
4
  import { fileExists, readJson, readText, timestampForFile, writeJson, writeText } from "./common.mjs";
5
5
  import { normalizeEngineName } from "./engine_metadata.mjs";
6
+ import {
7
+ defaultObserverRetrySettings,
8
+ defaultSupervisorKeepAliveSettings,
9
+ defaultTerminalConcurrencySettings,
10
+ normalizeObserverRetrySettings,
11
+ normalizeSupervisorKeepAliveSettings,
12
+ normalizeTerminalConcurrencySettings,
13
+ } from "./runtime_settings.mjs";
6
14
 
7
15
  function defaultEmailNotificationSettings() {
8
16
  return {
@@ -24,15 +32,6 @@ function defaultEmailNotificationSettings() {
24
32
  };
25
33
  }
26
34
 
27
- function defaultTerminalConcurrencySettings() {
28
- return {
29
- enabled: true,
30
- visibleMax: 8,
31
- backgroundMax: 8,
32
- totalMax: 8,
33
- };
34
- }
35
-
36
35
  function defaultUserSettings() {
37
36
  return {
38
37
  defaultEngine: "",
@@ -42,29 +41,30 @@ function defaultUserSettings() {
42
41
  },
43
42
  runtime: {
44
43
  terminalConcurrency: defaultTerminalConcurrencySettings(),
44
+ observerRetry: defaultObserverRetrySettings(),
45
+ supervisorKeepAlive: defaultSupervisorKeepAliveSettings(),
45
46
  },
46
47
  };
47
48
  }
48
49
 
49
- function cloneJsonValue(value) {
50
- return JSON.parse(JSON.stringify(value));
51
- }
52
-
53
50
  function isPlainObject(value) {
54
51
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
55
52
  }
56
53
 
57
- function syncValueBySchema(schemaValue, currentValue) {
58
- if (!isPlainObject(schemaValue)) {
59
- return currentValue === undefined ? cloneJsonValue(schemaValue) : currentValue;
60
- }
54
+ function normalizeString(value, fallback = "") {
55
+ return typeof value === "string" ? value.trim() : fallback;
56
+ }
61
57
 
62
- const source = isPlainObject(currentValue) ? currentValue : {};
63
- const next = {};
64
- for (const [key, childSchema] of Object.entries(schemaValue)) {
65
- next[key] = syncValueBySchema(childSchema, Object.hasOwn(source, key) ? source[key] : undefined);
58
+ function normalizeBoolean(value, fallback = false) {
59
+ return typeof value === "boolean" ? value : fallback;
60
+ }
61
+
62
+ function normalizePositiveInteger(value, fallback, minimum = 1) {
63
+ const numericValue = Number(value);
64
+ if (!Number.isInteger(numericValue) || numericValue < minimum) {
65
+ return fallback;
66
66
  }
67
- return next;
67
+ return numericValue;
68
68
  }
69
69
 
70
70
  function mergeValueBySchema(schemaValue, baseValue, patchValue) {
@@ -87,16 +87,6 @@ function mergeValueBySchema(schemaValue, baseValue, patchValue) {
87
87
  return next;
88
88
  }
89
89
 
90
- export function syncUserSettingsShape(settings = {}) {
91
- return syncValueBySchema(defaultUserSettings(), settings);
92
- }
93
-
94
- function readRawUserSettingsDocument(options = {}) {
95
- const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
96
- const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
97
- return syncUserSettingsShape(settings);
98
- }
99
-
100
90
  export function resolveUserSettingsHome() {
101
91
  return String(process.env.HELLOLOOP_HOME || "").trim()
102
92
  || path.join(os.homedir(), ".helloloop");
@@ -113,32 +103,51 @@ function normalizeEmailNotificationSettings(emailSettings = {}) {
113
103
  const smtp = emailSettings?.smtp || {};
114
104
 
115
105
  return {
116
- ...defaults,
117
- ...emailSettings,
106
+ enabled: normalizeBoolean(emailSettings?.enabled, defaults.enabled),
118
107
  to: Array.isArray(emailSettings?.to)
119
108
  ? emailSettings.to.map((item) => String(item || "").trim()).filter(Boolean)
120
- : (typeof emailSettings?.to === "string" && emailSettings.to.trim() ? [emailSettings.to.trim()] : []),
109
+ : defaults.to,
110
+ from: normalizeString(emailSettings?.from, defaults.from),
121
111
  smtp: {
122
- ...defaults.smtp,
123
- ...smtp,
112
+ host: normalizeString(smtp?.host, defaults.smtp.host),
113
+ port: normalizePositiveInteger(smtp?.port, defaults.smtp.port),
114
+ secure: normalizeBoolean(smtp?.secure, defaults.smtp.secure),
115
+ starttls: normalizeBoolean(smtp?.starttls, defaults.smtp.starttls),
116
+ username: normalizeString(smtp?.username, defaults.smtp.username),
117
+ usernameEnv: normalizeString(smtp?.usernameEnv, defaults.smtp.usernameEnv),
118
+ password: typeof smtp?.password === "string" ? smtp.password : defaults.smtp.password,
119
+ passwordEnv: normalizeString(smtp?.passwordEnv, defaults.smtp.passwordEnv),
120
+ timeoutSeconds: normalizePositiveInteger(smtp?.timeoutSeconds, defaults.smtp.timeoutSeconds),
121
+ rejectUnauthorized: normalizeBoolean(smtp?.rejectUnauthorized, defaults.smtp.rejectUnauthorized),
124
122
  },
125
123
  };
126
124
  }
127
125
 
128
- export function loadUserSettingsDocument(options = {}) {
129
- const settings = readRawUserSettingsDocument(options);
130
-
126
+ export function syncUserSettingsShape(settings = {}) {
131
127
  return {
132
- ...settings,
133
- defaultEngine: normalizeEngineName(settings?.defaultEngine),
134
- lastSelectedEngine: normalizeEngineName(settings?.lastSelectedEngine),
128
+ defaultEngine: normalizeEngineName(settings?.defaultEngine) || "",
129
+ lastSelectedEngine: normalizeEngineName(settings?.lastSelectedEngine) || "",
135
130
  notifications: {
136
- ...(settings?.notifications || {}),
137
131
  email: normalizeEmailNotificationSettings(settings?.notifications?.email || {}),
138
132
  },
133
+ runtime: {
134
+ terminalConcurrency: normalizeTerminalConcurrencySettings(settings?.runtime?.terminalConcurrency || {}),
135
+ observerRetry: normalizeObserverRetrySettings(settings?.runtime?.observerRetry || {}),
136
+ supervisorKeepAlive: normalizeSupervisorKeepAliveSettings(settings?.runtime?.supervisorKeepAlive || {}),
137
+ },
139
138
  };
140
139
  }
141
140
 
141
+ function readRawUserSettingsDocument(options = {}) {
142
+ const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
143
+ const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
144
+ return syncUserSettingsShape(settings);
145
+ }
146
+
147
+ export function loadUserSettingsDocument(options = {}) {
148
+ return readRawUserSettingsDocument(options);
149
+ }
150
+
142
151
  export function loadUserSettings(options = {}) {
143
152
  const settings = loadUserSettingsDocument(options);
144
153
  return {
@@ -0,0 +1,12 @@
1
+ export function shouldPromptForEngineSelection(options = {}) {
2
+ if (options.yes) {
3
+ return false;
4
+ }
5
+ if (options.detach) {
6
+ return false;
7
+ }
8
+ if (process.env.HELLOLOOP_SUPERVISOR_ACTIVE === "1") {
9
+ return false;
10
+ }
11
+ return Boolean(process.stdout?.isTTY);
12
+ }