dominds 1.25.4 → 1.25.6

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.
@@ -204,6 +204,11 @@ function getMemoryPromptCopy(ctx) {
204
204
  ? `工作流:先做事 → 再提炼(\`update_reminder\`;必要时整理差遣牒追加条目/更新提案并诉请 \`@${ctx.taskdocMaintainerId}\` 合并写入)→ 然后 \`clear_mind\` 清空噪音。`
205
205
  : '工作流:停止扩张上下文 → 维护足够详尽的接续包提醒项(`add_reminder` 或 `update_reminder`,长度没有技术限制)→ 然后 `clear_mind` 开启新一程。',
206
206
  mainDialogWorkflowLine: '工作流:先做事 -> 再提炼(`update_reminder` + `mind_more(progress)`;需要压缩/删旧时先 `recall_taskdoc` 取得 `content_hash`,再用带 `previous_content_hash` 的 `change_mind`;要删除整章文件时用 `never_mind`)-> 然后 `clear_mind` 清空噪音。',
207
+ ...(ctx.isSideDialog
208
+ ? {}
209
+ : {
210
+ progressVcsOrderLine: '硬性顺序:先补 `progress`,再动 git。只要这次代码/文档改动准备进 git(add / commit / push),先确认 `progress` 已经写清当前状态、决策、阻塞和下一步;如果还没写清,就先补写。简单判断:改动会进仓库 + `progress` 还没跟上 = 先别动 git。`git status` 只用来确认,不是提交动作。不要把“提交了某个 commit”“push 了”写进 `progress`。',
211
+ }),
207
212
  contextHealthLine: contextHealthLineZh,
208
213
  taskdocLogLine: taskdocLogLineZh,
209
214
  };
@@ -239,6 +244,11 @@ function getMemoryPromptCopy(ctx) {
239
244
  ? `Workflow: do work → distill (\`update_reminder\`; when Taskdoc needs updates, draft append entries, a merged replacement, or a section deletion and ask \`@${ctx.taskdocMaintainerId}\`) → then \`clear_mind\` to drop noise.`
240
245
  : 'Workflow: stop expanding context → maintain sufficiently detailed continuation-package reminders (`add_reminder` or `update_reminder`, with no technical length limit) → then `clear_mind` to start a new course.',
241
246
  mainDialogWorkflowLine: 'Workflow: do work -> distill (`update_reminder` + `mind_more(progress)`; when compression/deletion is needed, first use `recall_taskdoc` to get `content_hash`, then use `change_mind` with `previous_content_hash`; use `never_mind` when removing a whole section file) -> then `clear_mind` to drop noise.',
247
+ ...(ctx.isSideDialog
248
+ ? {}
249
+ : {
250
+ progressVcsOrderLine: 'Hard order: update `progress` first, then use git. If this code/docs change is going into git (add / commit / push), make sure `progress` already says the current state, decisions, blockers, and next step; if not, write it first. Simple check: change will enter the repo + `progress` is behind = stop and update `progress` first. `git status` is only for checking, not a commit step. Do not write git events like “committed X” or “ran git push” into progress.',
251
+ }),
242
252
  contextHealthLine: contextHealthLineEn,
243
253
  taskdocLogLine: taskdocLogLineEn,
244
254
  };
@@ -264,6 +274,7 @@ function buildMemorySystemPrompt(ctx) {
264
274
  ...(ctx.agentHasTeamMemoryTools ? [copy.teamMemoryHintLine] : []),
265
275
  ...(ctx.agentHasPersonalMemoryTools ? [copy.personalMemoryHintLine] : []),
266
276
  ctx.isSideDialog ? copy.sideDialogWorkflowLine : copy.mainDialogWorkflowLine,
277
+ ...(copy.progressVcsOrderLine === undefined ? [] : [copy.progressVcsOrderLine]),
267
278
  copy.contextHealthLine,
268
279
  copy.taskdocLogLine,
269
280
  ].join('\n');
@@ -16,9 +16,13 @@ const util_1 = require("util");
16
16
  const time_1 = require("@longrun-ai/kernel/utils/time");
17
17
  const log_1 = require("../log");
18
18
  const dominds_running_version_1 = require("./dominds-running-version");
19
- const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
19
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
20
20
  const log = (0, log_1.createLogger)('dominds-self-update');
21
21
  const BACKGROUND_CHECK_INTERVAL_MS = 30 * 60 * 1000;
22
+ const LATEST_VERSION_CHECK_TIMEOUT_MS = 15000;
23
+ const RESTART_PORT_RELEASE_TIMEOUT_MS = 15000;
24
+ const RESTART_PORT_PROBE_INTERVAL_MS = 150;
25
+ const COMMAND_OUTPUT_LOG_LIMIT = 2000;
22
26
  const IDLE_RESTART_STATE = { kind: 'idle' };
23
27
  let runtimeConfig = null;
24
28
  let latestObservation = { kind: 'unknown' };
@@ -27,12 +31,6 @@ let installPromise = null;
27
31
  let restartPromise = null;
28
32
  let restartState = IDLE_RESTART_STATE;
29
33
  let broadcastStatusUpdate = null;
30
- function getNpmBin() {
31
- return process.platform === 'win32' ? 'npm.cmd' : 'npm';
32
- }
33
- function getNpxBin() {
34
- return process.platform === 'win32' ? 'npx.cmd' : 'npx';
35
- }
36
34
  function normalizeVersionString(value) {
37
35
  return value.trim().replace(/^v/i, '');
38
36
  }
@@ -74,17 +72,115 @@ function detectRunKind(mode) {
74
72
  }
75
73
  return 'npm_global';
76
74
  }
75
+ function hasInteractiveConsole() {
76
+ return Boolean(process.stdin.isTTY || process.stdout.isTTY || process.stderr.isTTY);
77
+ }
78
+ function getRestartHelperStdio() {
79
+ return hasInteractiveConsole() ? 'inherit' : 'ignore';
80
+ }
81
+ function getRestartPortProbeHost(host) {
82
+ if (host === '0.0.0.0')
83
+ return '127.0.0.1';
84
+ if (host === '::')
85
+ return '::1';
86
+ return host;
87
+ }
88
+ function truncateCommandOutput(value) {
89
+ const raw = typeof value === 'string' ? value.trim() : '';
90
+ if (raw.length <= COMMAND_OUTPUT_LOG_LIMIT)
91
+ return raw;
92
+ return `${raw.slice(0, COMMAND_OUTPUT_LOG_LIMIT)}...[truncated ${raw.length - COMMAND_OUTPUT_LOG_LIMIT} chars]`;
93
+ }
94
+ function formatPathEnvExcerpt(pathEnv) {
95
+ if (pathEnv === null || pathEnv.trim() === '')
96
+ return null;
97
+ const parts = pathEnv.split(path_1.default.delimiter).filter((part) => part.trim() !== '');
98
+ if (parts.length === 0)
99
+ return null;
100
+ const visibleParts = parts.slice(0, 8);
101
+ const preview = visibleParts.join(path_1.default.delimiter);
102
+ if (parts.length <= visibleParts.length)
103
+ return preview;
104
+ return `${preview}${path_1.default.delimiter}...[+${parts.length - visibleParts.length} more]`;
105
+ }
106
+ function getErrorProp(error, key) {
107
+ if (typeof error !== 'object' || error === null)
108
+ return undefined;
109
+ return error[key];
110
+ }
111
+ function extractCommandFailureDiagnostics(error) {
112
+ const code = getErrorProp(error, 'code');
113
+ const signal = getErrorProp(error, 'signal');
114
+ const killed = getErrorProp(error, 'killed');
115
+ const cmd = getErrorProp(error, 'cmd');
116
+ return {
117
+ message: error instanceof Error ? error.message : String(error),
118
+ cmd: typeof cmd === 'string' && cmd.trim() !== '' ? cmd : null,
119
+ code: typeof code === 'string' || typeof code === 'number' ? code : null,
120
+ signal: typeof signal === 'string' && signal.trim() !== '' ? signal : null,
121
+ killed: typeof killed === 'boolean' ? killed : null,
122
+ stdout: truncateCommandOutput(getErrorProp(error, 'stdout')),
123
+ stderr: truncateCommandOutput(getErrorProp(error, 'stderr')),
124
+ cwd: process.cwd(),
125
+ pathEnv: typeof process.env.PATH === 'string' ? process.env.PATH : null,
126
+ };
127
+ }
128
+ function formatCommandFailureForUi(diagnostics) {
129
+ const lines = [diagnostics.message];
130
+ if (diagnostics.cmd !== null) {
131
+ lines.push(`cmd: ${diagnostics.cmd}`);
132
+ }
133
+ if (diagnostics.code !== null) {
134
+ lines.push(`exit: ${String(diagnostics.code)}`);
135
+ }
136
+ if (diagnostics.signal !== null) {
137
+ lines.push(`signal: ${diagnostics.signal}`);
138
+ }
139
+ lines.push(`cwd: ${diagnostics.cwd}`);
140
+ if (diagnostics.stderr !== '') {
141
+ lines.push(`stderr: ${diagnostics.stderr}`);
142
+ return lines.join('\n');
143
+ }
144
+ if (diagnostics.stdout !== '') {
145
+ lines.push(`stdout: ${diagnostics.stdout}`);
146
+ return lines.join('\n');
147
+ }
148
+ const pathEnvExcerpt = formatPathEnvExcerpt(diagnostics.pathEnv);
149
+ if (pathEnvExcerpt !== null) {
150
+ lines.push(`PATH: ${pathEnvExcerpt}`);
151
+ }
152
+ const lowerMessage = diagnostics.message.toLowerCase();
153
+ if (diagnostics.code === 'ENOENT' ||
154
+ lowerMessage.includes('not recognized as an internal or external command') ||
155
+ lowerMessage.includes('spawn npm enoent')) {
156
+ lines.push('hint: npm is not available to the server process on PATH');
157
+ }
158
+ else if (lowerMessage.includes('econn') ||
159
+ lowerMessage.includes('etimedout') ||
160
+ lowerMessage.includes('certificate') ||
161
+ lowerMessage.includes('self signed') ||
162
+ lowerMessage.includes('registry')) {
163
+ lines.push('hint: this looks like a registry, network, proxy, or certificate problem');
164
+ }
165
+ else if (diagnostics.code === 1 &&
166
+ diagnostics.stderr === '' &&
167
+ diagnostics.stdout === '') {
168
+ lines.push('hint: npm exited without output; check registry access and npm config in the same shell');
169
+ }
170
+ return lines.join('\n');
171
+ }
77
172
  async function queryLatestVersion() {
78
173
  const checkedAt = (0, time_1.formatUnifiedTimestamp)(new Date());
79
174
  try {
80
- const { stdout } = await execFileAsync(getNpmBin(), ['view', 'dominds', 'version', '--json'], {
175
+ const { stdout } = await execAsync('npm view dominds version --json', {
81
176
  cwd: process.cwd(),
82
177
  env: process.env,
83
178
  maxBuffer: 1024 * 1024,
84
- timeout: 15000,
179
+ timeout: LATEST_VERSION_CHECK_TIMEOUT_MS,
85
180
  });
86
181
  const trimmed = stdout.trim();
87
182
  if (trimmed === '') {
183
+ log.warn('Dominds latest-version check returned empty stdout', undefined, { checkedAt });
88
184
  return {
89
185
  kind: 'error',
90
186
  errorText: 'npm view dominds version returned empty stdout',
@@ -100,6 +196,10 @@ async function queryLatestVersion() {
100
196
  }
101
197
  const latestVersion = typeof parsed === 'string' ? parsed.trim() : '';
102
198
  if (latestVersion === '') {
199
+ log.warn('Dominds latest-version check returned an invalid payload', undefined, {
200
+ checkedAt,
201
+ stdout: trimmed,
202
+ });
103
203
  return {
104
204
  kind: 'error',
105
205
  errorText: `npm view dominds version returned non-string output: ${trimmed}`,
@@ -109,15 +209,20 @@ async function queryLatestVersion() {
109
209
  return { kind: 'ok', latestVersion, checkedAt };
110
210
  }
111
211
  catch (error) {
212
+ const diagnostics = extractCommandFailureDiagnostics(error);
213
+ const errorText = error instanceof Error && error.name === 'AbortError'
214
+ ? 'npm view dominds version timed out'
215
+ : formatCommandFailureForUi(diagnostics);
216
+ log.warn('Dominds latest-version check failed', error, { checkedAt, errorText, diagnostics });
112
217
  return {
113
218
  kind: 'error',
114
- errorText: error instanceof Error ? error.message : String(error),
219
+ errorText,
115
220
  checkedAt,
116
221
  };
117
222
  }
118
223
  }
119
224
  async function resolveGlobalDomindsCommandAbs() {
120
- const { stdout } = await execFileAsync(getNpmBin(), ['prefix', '-g'], {
225
+ const { stdout } = await execAsync('npm prefix -g', {
121
226
  cwd: process.cwd(),
122
227
  env: process.env,
123
228
  maxBuffer: 1024 * 1024,
@@ -422,7 +527,7 @@ async function installLatestDominds() {
422
527
  if (!hasUpdate) {
423
528
  throw new Error('No installable Dominds update is currently available');
424
529
  }
425
- await execFileAsync(getNpmBin(), ['i', '-g', 'dominds@latest'], {
530
+ await execAsync('npm i -g dominds@latest', {
426
531
  cwd: process.cwd(),
427
532
  env: process.env,
428
533
  maxBuffer: 20 * 1024 * 1024,
@@ -435,7 +540,19 @@ async function installLatestDominds() {
435
540
  globalCommandAbs,
436
541
  };
437
542
  return await getDomindsSelfUpdateStatus();
438
- })().finally(() => {
543
+ })()
544
+ .catch((error) => {
545
+ const latestVersion = latestObservation.kind === 'ok' ? latestObservation.latestVersion : null;
546
+ const checkedAt = latestObservation.kind === 'unknown' ? null : latestObservation.checkedAt;
547
+ log.error('Dominds version install failed', error, {
548
+ runKind,
549
+ currentVersion: getRunningVersion(),
550
+ latestVersion,
551
+ checkedAt,
552
+ });
553
+ throw error;
554
+ })
555
+ .finally(() => {
439
556
  installPromise = null;
440
557
  publishStatusUpdateSoon();
441
558
  });
@@ -446,33 +563,72 @@ function buildRestartArgs(cfg) {
446
563
  return ['webui', '-p', String(cfg.port), '-h', cfg.host, '--mode', 'prod', '--nobrowser'];
447
564
  }
448
565
  function spawnDetachedRestartHelper(params) {
566
+ const stdioMode = getRestartHelperStdio();
449
567
  const helperPayload = JSON.stringify({
450
568
  command: params.command,
451
569
  args: [...params.args],
452
570
  cwd: params.cwd,
453
- delayMs: 800,
571
+ host: getRestartPortProbeHost(params.host),
572
+ port: params.port,
573
+ probeIntervalMs: RESTART_PORT_PROBE_INTERVAL_MS,
574
+ portReleaseTimeoutMs: RESTART_PORT_RELEASE_TIMEOUT_MS,
575
+ stdioMode,
454
576
  });
455
577
  const helperScript = [
578
+ "const net = require('net');",
456
579
  "const { spawn } = require('child_process');",
457
580
  'const payload = JSON.parse(process.argv[1]);',
458
- 'setTimeout(() => {',
581
+ 'const detached = payload.stdioMode !== "inherit" || process.platform === "win32";',
582
+ 'function isPortBusy() {',
583
+ ' return new Promise((resolve) => {',
584
+ ' const socket = net.createConnection({ host: payload.host, port: payload.port });',
585
+ ' let settled = false;',
586
+ ' const finish = (busy) => {',
587
+ ' if (settled) return;',
588
+ ' settled = true;',
589
+ ' socket.destroy();',
590
+ ' resolve(busy);',
591
+ ' };',
592
+ ' socket.once("connect", () => finish(true));',
593
+ ' socket.once("error", () => finish(false));',
594
+ ' socket.setTimeout(1000, () => finish(true));',
595
+ ' });',
596
+ '}',
597
+ 'async function waitForPortRelease() {',
598
+ ' const deadline = Date.now() + payload.portReleaseTimeoutMs;',
599
+ ' let consecutiveIdle = 0;',
600
+ ' while (Date.now() < deadline) {',
601
+ ' if (await isPortBusy()) {',
602
+ ' consecutiveIdle = 0;',
603
+ ' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
604
+ ' continue;',
605
+ ' }',
606
+ ' consecutiveIdle += 1;',
607
+ ' if (consecutiveIdle >= 2) return;',
608
+ ' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
609
+ ' }',
610
+ '}',
611
+ '(async () => {',
459
612
  ' try {',
460
- " const child = spawn(payload.command, payload.args, { cwd: payload.cwd, env: process.env, detached: true, stdio: 'ignore' });",
461
- ' child.unref();',
613
+ ' await waitForPortRelease();',
614
+ " const child = spawn(payload.command, payload.args, { cwd: payload.cwd, env: process.env, detached, stdio: payload.stdioMode, shell: process.platform === 'win32' });",
615
+ ' if (detached) child.unref();',
462
616
  ' process.exit(0);',
463
617
  ' } catch (error) {',
464
618
  ' console.error(error instanceof Error ? error.message : String(error));',
465
619
  ' process.exit(1);',
466
620
  ' }',
467
- '}, payload.delayMs);',
621
+ '})();',
468
622
  ].join('\n');
469
623
  const helper = (0, child_process_1.spawn)(process.execPath, ['-e', helperScript, helperPayload], {
470
624
  cwd: params.cwd,
471
625
  env: process.env,
472
- detached: true,
473
- stdio: 'ignore',
626
+ detached: stdioMode !== 'inherit' || process.platform === 'win32',
627
+ stdio: stdioMode,
474
628
  });
475
- helper.unref();
629
+ if (stdioMode !== 'inherit' || process.platform === 'win32') {
630
+ helper.unref();
631
+ }
476
632
  }
477
633
  async function stopAndExitForRestart() {
478
634
  const cfg = assertRuntimeConfig();
@@ -500,7 +656,7 @@ async function restartDomindsIntoLatest() {
500
656
  let command;
501
657
  const previousRestartRequiredState = restartState.kind === 'restart_required' ? restartState : null;
502
658
  if (runKind === 'npx_latest') {
503
- command = getNpxBin();
659
+ command = 'npx';
504
660
  args.unshift('dominds@latest');
505
661
  args.unshift('-y');
506
662
  }
@@ -512,25 +668,48 @@ async function restartDomindsIntoLatest() {
512
668
  }
513
669
  restartState = { kind: 'restarting', targetVersion: status.targetVersion };
514
670
  publishStatusUpdateSoon();
515
- spawnDetachedRestartHelper({
516
- command,
517
- args,
518
- cwd: process.cwd(),
519
- });
520
- setImmediate(() => {
521
- void stopAndExitForRestart().catch((error) => {
522
- log.error('Failed to stop Dominds server during restart', error);
523
- if (runKind === 'npm_global' && previousRestartRequiredState !== null) {
524
- restartState = previousRestartRequiredState;
671
+ try {
672
+ spawnDetachedRestartHelper({
673
+ command,
674
+ args,
675
+ cwd: process.cwd(),
676
+ host: cfg.host,
677
+ port: cfg.port,
678
+ });
679
+ setImmediate(() => {
680
+ void stopAndExitForRestart().catch((error) => {
681
+ log.error('Failed to stop Dominds server during restart', error);
682
+ if (runKind === 'npm_global' && previousRestartRequiredState !== null) {
683
+ restartState = previousRestartRequiredState;
684
+ publishStatusUpdateSoon();
685
+ return;
686
+ }
687
+ restartState = IDLE_RESTART_STATE;
525
688
  publishStatusUpdateSoon();
526
- return;
527
- }
528
- restartState = IDLE_RESTART_STATE;
529
- publishStatusUpdateSoon();
689
+ });
530
690
  });
531
- });
691
+ }
692
+ catch (error) {
693
+ if (previousRestartRequiredState !== null) {
694
+ restartState = previousRestartRequiredState;
695
+ }
696
+ else {
697
+ restartState = IDLE_RESTART_STATE;
698
+ }
699
+ publishStatusUpdateSoon();
700
+ throw error;
701
+ }
532
702
  return await getDomindsSelfUpdateStatus();
533
- })().finally(() => {
703
+ })()
704
+ .catch((error) => {
705
+ const statusSnapshot = restartState.kind === 'restarting' ? restartState : null;
706
+ log.error('Dominds version restart failed', error, {
707
+ runKind: detectRunKind(cfg.mode),
708
+ restartState: statusSnapshot,
709
+ });
710
+ throw error;
711
+ })
712
+ .finally(() => {
534
713
  restartPromise = null;
535
714
  publishStatusUpdateSoon();
536
715
  });
@@ -4,6 +4,7 @@
4
4
  * Operating system interaction tools for shell command execution.
5
5
  * Provides shell_cmd and stop_daemon FuncTools with advanced process management.
6
6
  */
7
+ import type { LanguageCode } from '@longrun-ai/kernel/types/language';
7
8
  import type { FuncTool, ReminderOwner } from '../tool';
8
9
  export declare function resetTrackedDaemonsForTests(): void;
9
10
  type ShellSpawnSpec = Readonly<{
@@ -13,6 +14,7 @@ type ShellSpawnSpec = Readonly<{
13
14
  windowsVerbatimArguments?: boolean;
14
15
  }>;
15
16
  export declare function resolveShellCmdSpawnSpecForTests(command: string, shell: string | undefined, platform: NodeJS.Platform): ShellSpawnSpec;
17
+ export declare function detectWindowsShellUsageWarningForTests(command: string, shell: string | undefined, language: LanguageCode, platform: NodeJS.Platform): string | undefined;
16
18
  export declare function resolveReadonlyShellSpawnSpecForTests(command: string, platform: NodeJS.Platform): ShellSpawnSpec;
17
19
  export declare const shellCmdReminderOwner: ReminderOwner;
18
20
  export declare const shellCmdTool: FuncTool;
package/dist/tools/os.js CHANGED
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getDaemonOutputTool = exports.stopDaemonTool = exports.readonlyShellTool = exports.shellCmdTool = exports.shellCmdReminderOwner = void 0;
13
13
  exports.resetTrackedDaemonsForTests = resetTrackedDaemonsForTests;
14
14
  exports.resolveShellCmdSpawnSpecForTests = resolveShellCmdSpawnSpecForTests;
15
+ exports.detectWindowsShellUsageWarningForTests = detectWindowsShellUsageWarningForTests;
15
16
  exports.resolveReadonlyShellSpawnSpecForTests = resolveReadonlyShellSpawnSpecForTests;
16
17
  const time_1 = require("@longrun-ai/kernel/utils/time");
17
18
  const child_process_1 = require("child_process");
@@ -645,7 +646,7 @@ async function spawnCmdRunner(init) {
645
646
  runnerProcess.send(init);
646
647
  });
647
648
  }
648
- function formatCompletedShellCommandOutput(message, t) {
649
+ function formatCompletedShellCommandOutput(message, t, warning) {
649
650
  const stdoutHasScrolled = message.stdout.linesScrolledOut > 0;
650
651
  const stderrHasScrolled = message.stderr.linesScrolledOut > 0;
651
652
  let scrollNotice = '';
@@ -667,7 +668,7 @@ function formatCompletedShellCommandOutput(message, t) {
667
668
  if (stderrContent !== '') {
668
669
  result += `${t.stderrLabel}\n${fenceConsole}\n${stderrContent}\n${fenceEnd}`;
669
670
  }
670
- const content = result.trim();
671
+ const content = prependShellWarning(result.trim(), warning);
671
672
  return message.exitCode === 0 ? (0, tool_1.toolSuccess)(content) : (0, tool_1.toolPartialFailure)(content);
672
673
  }
673
674
  async function removeDaemonRemindersForPid(dlg, pid) {
@@ -818,76 +819,42 @@ function parseGetDaemonOutputArgs(args) {
818
819
  }
819
820
  return { pid, stdout, stderr };
820
821
  }
821
- function encodePowerShellCommand(command) {
822
- return Buffer.from(command, 'utf16le').toString('base64');
823
- }
824
- function stripMatchingOuterQuotes(value) {
825
- const trimmed = value.trim();
826
- if (trimmed.length < 2) {
827
- return trimmed;
828
- }
829
- const first = trimmed[0] ?? '';
830
- const last = trimmed[trimmed.length - 1] ?? '';
831
- if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
832
- return trimmed.slice(1, -1);
833
- }
834
- return trimmed;
835
- }
836
- function resolveDirectWindowsPowerShellCommand(command) {
837
- const match = /^\s*(powershell(?:\.exe)?|pwsh(?:\.exe)?)\s+(?:-(?:NoLogo|NoProfile|NonInteractive)\s+)*(?:-|\/)(?:Command|c)\s+([\s\S]+?)\s*$/iu.exec(command);
838
- if (match === null) {
839
- return null;
822
+ function getWindowsShellLabel(shell) {
823
+ if (typeof shell !== 'string') {
824
+ return 'cmd.exe';
840
825
  }
841
- const shellCommand = match[1] ?? '';
842
- const encodedCommand = encodePowerShellCommand(stripMatchingOuterQuotes(match[2] ?? ''));
843
- return {
844
- command: shellCommand,
845
- args: ['-NoLogo', '-NoProfile', '-EncodedCommand', encodedCommand],
846
- shellLabel: shellCommand,
847
- };
826
+ const trimmed = shell.trim();
827
+ return trimmed === '' ? 'cmd.exe' : trimmed;
848
828
  }
849
- function unwrapNestedWindowsCmd(command) {
850
- const match = /^\s*cmd(?:\.exe)?\s+(?:\/d\s+)?(?:\/s\s+)?\/c\s+([\s\S]+?)\s*$/iu.exec(command);
851
- if (match === null) {
852
- return null;
853
- }
854
- const inner = (match[1] ?? '').trim();
855
- if (inner.length < 2) {
856
- return inner;
857
- }
858
- const first = inner[0] ?? '';
859
- const last = inner[inner.length - 1] ?? '';
860
- if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
861
- return inner.slice(1, -1);
829
+ function detectWindowsShellUsageWarning(command, shell, language, platform = process.platform) {
830
+ if (platform !== 'win32') {
831
+ return undefined;
862
832
  }
863
- return inner;
864
- }
865
- function normalizeWindowsCmdCommand(command) {
866
- const directPowerShell = resolveDirectWindowsPowerShellCommand(command);
867
- if (directPowerShell !== null) {
868
- return command;
833
+ const trimmedCommand = command.trimStart();
834
+ const nestedCmd = /^cmd(?:\.exe)?\s+(?:\/d\s+)?(?:\/s\s+)?\/c\b/iu.test(trimmedCommand);
835
+ const nestedPowerShell = /^(?:powershell(?:\.exe)?|pwsh(?:\.exe)?)\s+(?:-(?:NoLogo|NoProfile|NonInteractive)\s+)*(?:-|\/)(?:Command|c)\b/iu.test(trimmedCommand);
836
+ if (!nestedCmd && !nestedPowerShell) {
837
+ return undefined;
869
838
  }
870
- const nestedCmd = unwrapNestedWindowsCmd(command);
871
- if (nestedCmd !== null) {
872
- return nestedCmd.replace(/\\"/g, '"').replace(/^(\s*if\s+(?:not\s+)?exist\s+)'([^'\r\n]+)'/iu, (_whole, prefix, quotedPath) => `${prefix}"${normalizeWindowsPathLiteral(quotedPath)}"`);
839
+ const shellLabel = getWindowsShellLabel(shell);
840
+ if (language === 'zh') {
841
+ return nestedCmd
842
+ ? `⚠️ 检测到嵌套 shell 写法:${trimmedCommand.startsWith('cmd.exe') ? 'cmd.exe /c' : 'cmd /c'}。shell 参数只负责选择外层执行环境;请直接传入 cmd 原生命令,不要再套一层 cmd /c。当前 shell:${shellLabel}`
843
+ : `⚠️ 检测到嵌套 shell 写法:${trimmedCommand.startsWith('pwsh') ? 'pwsh -Command' : 'powershell -Command'}。shell 参数只负责选择外层执行环境;请直接传入 PowerShell 原生命令,不要再套一层 -Command。当前 shell:${shellLabel}`;
873
844
  }
874
- return command.replace(/\\"/g, '"').replace(/^(\s*if\s+(?:not\s+)?exist\s+)'([^'\r\n]+)'/iu, (_whole, prefix, quotedPath) => `${prefix}"${normalizeWindowsPathLiteral(quotedPath)}"`);
845
+ return nestedCmd
846
+ ? `⚠️ Nested shell syntax detected: ${trimmedCommand.startsWith('cmd.exe') ? 'cmd.exe /c' : 'cmd /c'}. The shell parameter selects the outer execution environment only; pass a native cmd command and do not add another cmd /c layer. Current shell: ${shellLabel}`
847
+ : `⚠️ Nested shell syntax detected: ${trimmedCommand.startsWith('pwsh') ? 'pwsh -Command' : 'powershell -Command'}. The shell parameter selects the outer execution environment only; pass a native PowerShell command and do not add another -Command wrapper. Current shell: ${shellLabel}`;
875
848
  }
876
- function normalizeWindowsPathLiteral(pathLiteral) {
877
- if (pathLiteral.startsWith('\\\\')) {
878
- return pathLiteral;
849
+ function prependShellWarning(content, warning) {
850
+ if (!warning) {
851
+ return content;
879
852
  }
880
- return pathLiteral.replace(/\\{2,}/g, '\\');
853
+ return `${warning}\n\n${content}`;
881
854
  }
882
855
  function resolveShellCmdSpawnSpec(command, shell, platform = process.platform) {
883
856
  const preferredShell = typeof shell === 'string' && shell.trim() !== '' ? shell.trim() : undefined;
884
857
  if (platform === 'win32') {
885
- if (!preferredShell) {
886
- const directPowerShell = resolveDirectWindowsPowerShellCommand(command);
887
- if (directPowerShell !== null) {
888
- return directPowerShell;
889
- }
890
- }
891
858
  if (preferredShell) {
892
859
  const base = path_1.default.basename(preferredShell).toLowerCase();
893
860
  if (base === 'powershell' ||
@@ -896,14 +863,14 @@ function resolveShellCmdSpawnSpec(command, shell, platform = process.platform) {
896
863
  base === 'pwsh.exe') {
897
864
  return {
898
865
  command: preferredShell,
899
- args: ['-NoLogo', '-NoProfile', '-EncodedCommand', encodePowerShellCommand(command)],
866
+ args: ['-NoLogo', '-NoProfile', '-Command', command],
900
867
  shellLabel: preferredShell,
901
868
  };
902
869
  }
903
870
  if (base === 'cmd' || base === 'cmd.exe') {
904
871
  return {
905
872
  command: preferredShell,
906
- args: ['/d', '/c', normalizeWindowsCmdCommand(command)],
873
+ args: ['/d', '/c', command],
907
874
  shellLabel: preferredShell,
908
875
  windowsVerbatimArguments: true,
909
876
  };
@@ -916,7 +883,7 @@ function resolveShellCmdSpawnSpec(command, shell, platform = process.platform) {
916
883
  }
917
884
  return {
918
885
  command: 'cmd.exe',
919
- args: ['/d', '/c', normalizeWindowsCmdCommand(command)],
886
+ args: ['/d', '/c', command],
920
887
  shellLabel: 'cmd.exe',
921
888
  windowsVerbatimArguments: true,
922
889
  };
@@ -931,11 +898,14 @@ function resolveShellCmdSpawnSpec(command, shell, platform = process.platform) {
931
898
  function resolveShellCmdSpawnSpecForTests(command, shell, platform) {
932
899
  return resolveShellCmdSpawnSpec(command, shell, platform);
933
900
  }
901
+ function detectWindowsShellUsageWarningForTests(command, shell, language, platform) {
902
+ return detectWindowsShellUsageWarning(command, shell, language, platform);
903
+ }
934
904
  function resolveReadonlyShellSpawnSpec(command, platform = process.platform) {
935
905
  if (platform === 'win32') {
936
906
  return {
937
907
  command: 'cmd.exe',
938
- args: ['/d', '/c', normalizeWindowsCmdCommand(command)],
908
+ args: ['/d', '/c', command],
939
909
  shellLabel: 'cmd.exe',
940
910
  windowsVerbatimArguments: true,
941
911
  };
@@ -1187,7 +1157,7 @@ const shellCmdSchema = {
1187
1157
  },
1188
1158
  shell: {
1189
1159
  type: 'string',
1190
- description: 'Shell to use for execution (default: bash on Linux/macOS; cmd.exe on Windows). On Windows, powershell.exe/pwsh commands are passed via -EncodedCommand.',
1160
+ description: 'Shell to use for execution (default: bash on Linux/macOS; cmd.exe on Windows). On Windows, choose cmd.exe, powershell.exe, or pwsh explicitly; command must be native to the selected shell.',
1191
1161
  },
1192
1162
  scrollbackLines: {
1193
1163
  type: 'number',
@@ -1536,10 +1506,10 @@ ${statusInfo}`,
1536
1506
  exports.shellCmdTool = {
1537
1507
  type: 'func',
1538
1508
  name: 'shell_cmd',
1539
- description: 'Execute shell commands with optional timeout. If timeoutSeconds > 0 and command runs longer, it becomes a tracked daemon process. Daemons persist across messages and require explicit stop_daemon or get_daemon_output calls.',
1509
+ description: 'Execute shell commands with optional timeout. If timeoutSeconds > 0 and command runs longer, it becomes a tracked daemon process. On Windows, use shell to choose cmd.exe, powershell.exe, or pwsh, and pass a command that is native to that shell. Do not nest cmd /c or powershell -Command inside another shell command. Daemons persist across messages and require explicit stop_daemon or get_daemon_output calls.',
1540
1510
  descriptionI18n: {
1541
- en: 'Execute shell commands with optional timeout. If timeoutSeconds > 0 and command runs longer, it becomes a tracked daemon process. Daemons persist across messages and require explicit stop_daemon or get_daemon_output calls.',
1542
- zh: '执行 shell 命令(支持超时)。如果 timeoutSeconds > 0 且命令运行时间超过超时,将转为可追踪的后台守护进程。守护进程会跨消息持续存在,需要显式调用 stop_daemon 或 get_daemon_output 来管理与查看输出。',
1511
+ en: 'Execute shell commands with optional timeout. If timeoutSeconds > 0 and command runs longer, it becomes a tracked daemon process. On Windows, use shell to choose cmd.exe, powershell.exe, or pwsh, and pass a command that is native to that shell. Do not nest cmd /c or powershell -Command inside another shell command. Daemons persist across messages and require explicit stop_daemon or get_daemon_output calls.',
1512
+ zh: '执行 shell 命令(支持超时)。如果 timeoutSeconds > 0 且命令运行时间超过超时,将转为可追踪的后台守护进程。Windows 上请用 shell 明确选择 cmd.exe、powershell.exe 或 pwsh,并传入该 shell 的原生命令。不要在另一个 shell 命令里再嵌套 cmd /c 或 powershell -Command。守护进程会跨消息持续存在,需要显式调用 stop_daemon 或 get_daemon_output 来管理与查看输出。',
1543
1513
  },
1544
1514
  parameters: shellCmdSchema,
1545
1515
  async call(dlg, caller, args) {
@@ -1548,6 +1518,7 @@ exports.shellCmdTool = {
1548
1518
  const parsedArgs = parseShellCmdArgs(args);
1549
1519
  const { command, shell, scrollbackLines = 500, timeoutSeconds = 5 } = parsedArgs;
1550
1520
  const spawnSpec = resolveShellCmdSpawnSpec(command, shell);
1521
+ const warning = detectWindowsShellUsageWarning(command, shell, language);
1551
1522
  try {
1552
1523
  const { runnerProcess, initialMessage } = await spawnCmdRunner({
1553
1524
  type: 'init',
@@ -1564,11 +1535,11 @@ exports.shellCmdTool = {
1564
1535
  scrollbackLines,
1565
1536
  });
1566
1537
  if (initialMessage.type === 'completed') {
1567
- return formatCompletedShellCommandOutput(initialMessage, t);
1538
+ return formatCompletedShellCommandOutput(initialMessage, t, warning);
1568
1539
  }
1569
1540
  if (initialMessage.type === 'failed') {
1570
1541
  disconnectRunnerProcess(runnerProcess);
1571
- return (0, tool_1.toolFailure)(t.failedToExecute(initialMessage.errorText));
1542
+ return (0, tool_1.toolFailure)(prependShellWarning(t.failedToExecute(initialMessage.errorText), warning));
1572
1543
  }
1573
1544
  const daemon = {
1574
1545
  pid: initialMessage.daemonPid,
@@ -1620,15 +1591,15 @@ exports.shellCmdTool = {
1620
1591
  catch (error) {
1621
1592
  await bestEffortKillDaemonProcessGroup(reminderSeedMeta);
1622
1593
  disconnectRunnerProcess(runnerProcess);
1623
- return (0, tool_1.toolFailure)(t.failedToExecute(error instanceof Error
1594
+ return (0, tool_1.toolFailure)(prependShellWarning(t.failedToExecute(error instanceof Error
1624
1595
  ? `daemon reminder persistence failed: ${error.message}`
1625
- : `daemon reminder persistence failed: ${String(error)}`));
1596
+ : `daemon reminder persistence failed: ${String(error)}`), warning));
1626
1597
  }
1627
1598
  disconnectRunnerProcess(runnerProcess);
1628
- return (0, tool_1.toolSuccess)(t.daemonStarted(initialMessage.daemonPid, timeoutSeconds, command));
1599
+ return (0, tool_1.toolSuccess)(prependShellWarning(t.daemonStarted(initialMessage.daemonPid, timeoutSeconds, command), warning));
1629
1600
  }
1630
1601
  catch (error) {
1631
- return (0, tool_1.toolFailure)(t.failedToExecute(error instanceof Error ? error.message : String(error)));
1602
+ return (0, tool_1.toolFailure)(prependShellWarning(t.failedToExecute(error instanceof Error ? error.message : String(error)), warning));
1632
1603
  }
1633
1604
  },
1634
1605
  };
@@ -2209,10 +2180,11 @@ exports.readonlyShellTool = {
2209
2180
  const t = getOsToolMessages(language);
2210
2181
  const parsedArgs = parseReadonlyShellArgs(args);
2211
2182
  const { command, timeoutMs = 10000 } = parsedArgs;
2183
+ const warning = detectWindowsShellUsageWarning(command, undefined, language);
2212
2184
  if (command.includes('\n') || command.includes('\r')) {
2213
- return (0, tool_1.toolFailure)(language === 'zh'
2185
+ return (0, tool_1.toolFailure)(prependShellWarning(language === 'zh'
2214
2186
  ? `❌ readonly_shell 不建议执行多行脚本式命令(检测到换行符)。请用单行命令(允许 |、&&、||)。\n收到:${command}`
2215
- : `❌ readonly_shell does not allow multi-line script-style commands (newline detected). Use a single-line command (|, &&, || are allowed).\nGot: ${command}`);
2187
+ : `❌ readonly_shell does not allow multi-line script-style commands (newline detected). Use a single-line command (|, &&, || are allowed).\nGot: ${command}`, warning));
2216
2188
  }
2217
2189
  const validation = validateReadonlyShellCommand(command);
2218
2190
  if (!validation.ok) {
@@ -2222,9 +2194,9 @@ exports.readonlyShellTool = {
2222
2194
  const suggestion = language === 'zh'
2223
2195
  ? getReadonlyShellSuggestionZh(validation.failure)
2224
2196
  : getReadonlyShellSuggestionEn(validation.failure);
2225
- return (0, tool_1.toolFailure)(language === 'zh'
2197
+ return (0, tool_1.toolFailure)(prependShellWarning(language === 'zh'
2226
2198
  ? `❌ readonly_shell 仅允许以下命令前缀:${allowedList}\n另外允许(仅版本探针):node --version|-v、python3 --version|-V\n脚本执行(如 node -e / python3 -c)一律拒绝。\n另外允许:git -C <相对路径> <show|status|diff|log|blame> ...\n另外允许:cd <相对路径> && <允许命令...>(或 ||)\n说明:通过 |/&&/|| 串联时会按子命令逐段校验。\n被拒子命令段:${rejectedSegmentOrCommand}\n允许的等价写法:${suggestion}\n收到:${command}`
2227
- : `❌ readonly_shell only allows these command prefixes: ${allowedList}\nAlso allowed (exact version probes only): node --version|-v, python3 --version|-V\nNode/python scripts (for example: node -e, python3 -c) are rejected.\nAlso allowed: git -C <relative-path> <show|status|diff|log|blame> ...\nAlso allowed: cd <relative-path> && <allowed command...> (or ||)\nNote: chains via |/&&/|| are validated segment-by-segment.\nRejected segment: ${rejectedSegmentOrCommand}\nAllowed equivalent: ${suggestion}\nGot: ${command}`);
2199
+ : `❌ readonly_shell only allows these command prefixes: ${allowedList}\nAlso allowed (exact version probes only): node --version|-v, python3 --version|-V\nNode/python scripts (for example: node -e, python3 -c) are rejected.\nAlso allowed: git -C <relative-path> <show|status|diff|log|blame> ...\nAlso allowed: cd <relative-path> && <allowed command...> (or ||)\nNote: chains via |/&&/|| are validated segment-by-segment.\nRejected segment: ${rejectedSegmentOrCommand}\nAllowed equivalent: ${suggestion}\nGot: ${command}`, warning));
2228
2200
  }
2229
2201
  const forbiddenHiddenDir = detectReadonlyShellForbiddenHiddenDirAccess(path_1.default.resolve(process.cwd()), command);
2230
2202
  if (forbiddenHiddenDir) {
@@ -2277,7 +2249,7 @@ exports.readonlyShellTool = {
2277
2249
  ? `⚠️ 输出已截断,约省略 ${omittedBytes} 字节\n`
2278
2250
  : `⚠️ Output truncated; ~${omittedBytes} bytes omitted\n`
2279
2251
  : '';
2280
- let result = `${timeoutMsg}${truncationNotice}`.trimEnd();
2252
+ let result = prependShellWarning(`${timeoutMsg}${truncationNotice}`.trimEnd(), warning);
2281
2253
  const stdoutContent = (0, output_limit_1.truncateToolOutputText)(stdoutBuffer.getContent(), {
2282
2254
  toolName: 'readonly_shell_stdout',
2283
2255
  }).text;
@@ -2308,7 +2280,7 @@ exports.readonlyShellTool = {
2308
2280
  }).text;
2309
2281
  const fenceConsole = '```console';
2310
2282
  const fenceEnd = '```';
2311
- let result = t.commandCompleted(code, truncationNotice);
2283
+ let result = prependShellWarning(t.commandCompleted(code, truncationNotice), warning);
2312
2284
  if (stdoutContent) {
2313
2285
  result += `${t.stdoutLabel}\n${fenceConsole}\n${stdoutContent}\n${fenceEnd}\n\n`;
2314
2286
  }
@@ -2319,7 +2291,7 @@ exports.readonlyShellTool = {
2319
2291
  });
2320
2292
  childProcess.on('error', (error) => {
2321
2293
  clearTimeout(timeoutHandle);
2322
- finish((0, tool_1.toolFailure)(t.failedToExecute(error.message)));
2294
+ finish((0, tool_1.toolFailure)(prependShellWarning(t.failedToExecute(error.message), warning)));
2323
2295
  });
2324
2296
  });
2325
2297
  },
@@ -15,7 +15,8 @@ Typical uses:
15
15
  Windows notes:
16
16
 
17
17
  - Prefer no-space forward-slash paths such as `D:/path/to/file`
18
- - Avoid nested `cmd /c "..."`
18
+ - `readonly_shell` runs through the platform default shell; pass native commands for that shell
19
+ - Avoid nested `cmd /c` or `powershell -Command`; obvious nested-shell patterns may only warn and are not rewritten
19
20
 
20
21
  Example:
21
22
 
@@ -15,7 +15,8 @@
15
15
  Windows 注意:
16
16
 
17
17
  - 优先使用不带空格的正斜杠路径,如 `D:/path/to/file`
18
- - 避免嵌套 `cmd /c "..."`
18
+ - `readonly_shell` 通过平台默认 shell 执行;请传入该 shell 的原生命令
19
+ - 避免嵌套 `cmd /c` 或 `powershell -Command`;明显混套模式最多只会警告,不会被改写
19
20
 
20
21
  示例:
21
22
 
@@ -194,7 +194,7 @@ shell_cmd({
194
194
  command: 'if exist D:/AiWorks/chatgpt-workstation/dist/app.exe echo exists',
195
195
  });
196
196
 
197
- // Complex PowerShell command: choose PowerShell explicitly; Dominds encodes the command automatically
197
+ // Complex PowerShell command: choose PowerShell explicitly and keep the command native to that shell
198
198
  shell_cmd({
199
199
  command: 'Test-Path D:/AiWorks/chatgpt-workstation/dist/app.exe',
200
200
  shell: 'powershell.exe',
@@ -32,9 +32,10 @@ Execute Shell command.
32
32
 
33
33
  **Windows notes:**
34
34
 
35
+ - Use `shell` to choose the outer execution environment: `cmd.exe`, `powershell.exe`, or `pwsh`
36
+ - Pass a command that is native to the selected shell; do not nest `cmd /c` or `powershell -Command` inside another shell command
35
37
  - With the default `cmd.exe` path, prefer no-space forward-slash paths such as `D:/path/to/file`
36
- - For complex PowerShell commands, pass `shell: 'powershell.exe'` or `shell: 'pwsh'`; the command is encoded automatically to avoid quote and backslash ambiguity
37
- - Do not rely on nested `cmd /c "..."`
38
+ - Only very obvious nested-shell patterns may trigger a warning; the tool does not rewrite mixed shell syntax for you
38
39
 
39
40
  **Returns:**
40
41
 
@@ -194,7 +194,7 @@ shell_cmd({
194
194
  command: 'if exist D:/AiWorks/chatgpt-workstation/dist/app.exe echo exists',
195
195
  });
196
196
 
197
- // 复杂 PowerShell 命令:显式选择 PowerShell,系统会自动编码传递命令
197
+ // 复杂 PowerShell 命令:显式选择 PowerShell,并保持命令为该 shell 的原生命令
198
198
  shell_cmd({
199
199
  command: 'Test-Path D:/AiWorks/chatgpt-workstation/dist/app.exe',
200
200
  shell: 'powershell.exe',
@@ -32,9 +32,10 @@
32
32
 
33
33
  **Windows 提示:**
34
34
 
35
+ - 使用 `shell` 选择外层执行环境:`cmd.exe`、`powershell.exe` 或 `pwsh`
36
+ - `command` 必须是所选 shell 的原生命令;不要在另一个 shell 命令里嵌套 `cmd /c` 或 `powershell -Command`
35
37
  - 默认 `cmd.exe` 路径下,优先使用不带空格的正斜杠路径,如 `D:/path/to/file`
36
- - 复杂 PowerShell 命令建议显式传 `shell: 'powershell.exe'` 或 `shell: 'pwsh'`;系统会自动用编码方式传递命令,避免引号和反斜杠歧义
37
- - 不要依赖 `cmd /c "..."`
38
+ - 只有非常明显的混套模式可能触发警告;工具不会替你改写混合 shell 语法
38
39
 
39
40
  **返回:**
40
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dominds",
3
- "version": "1.25.4",
3
+ "version": "1.25.6",
4
4
  "description": "Dominds CLI and aggregation shell for the LongRun AI kernel/runtime packages.",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -53,8 +53,8 @@
53
53
  "yaml": "^2.8.2",
54
54
  "zod": "^4.3.6",
55
55
  "@longrun-ai/codex-auth": "0.13.0",
56
- "@longrun-ai/kernel": "1.15.4",
57
- "@longrun-ai/shell": "1.15.4"
56
+ "@longrun-ai/kernel": "1.15.5",
57
+ "@longrun-ai/shell": "1.15.5"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.3.5",