magi-ai 0.1.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.
- package/LICENSE +21 -0
- package/README.ja.md +377 -0
- package/README.md +377 -0
- package/dist/bin/magi-benchmark.d.ts +14 -0
- package/dist/bin/magi-benchmark.js +93 -0
- package/dist/bin/magi-mcp.d.ts +8 -0
- package/dist/bin/magi-mcp.js +28 -0
- package/dist/bin/magi.d.ts +2 -0
- package/dist/bin/magi.js +634 -0
- package/dist/src/adapters/base.d.ts +34 -0
- package/dist/src/adapters/base.js +149 -0
- package/dist/src/adapters/claude.d.ts +29 -0
- package/dist/src/adapters/claude.js +65 -0
- package/dist/src/adapters/codex.d.ts +21 -0
- package/dist/src/adapters/codex.js +41 -0
- package/dist/src/adapters/gemini.d.ts +18 -0
- package/dist/src/adapters/gemini.js +31 -0
- package/dist/src/adapters/registry.d.ts +19 -0
- package/dist/src/adapters/registry.js +59 -0
- package/dist/src/audit/hash-chain.d.ts +21 -0
- package/dist/src/audit/hash-chain.js +70 -0
- package/dist/src/audit/types.d.ts +25 -0
- package/dist/src/audit/types.js +1 -0
- package/dist/src/audit/writer.d.ts +18 -0
- package/dist/src/audit/writer.js +100 -0
- package/dist/src/benchmark/golden-tasks.d.ts +9 -0
- package/dist/src/benchmark/golden-tasks.js +476 -0
- package/dist/src/benchmark/reporter.d.ts +5 -0
- package/dist/src/benchmark/reporter.js +107 -0
- package/dist/src/benchmark/runner.d.ts +30 -0
- package/dist/src/benchmark/runner.js +224 -0
- package/dist/src/benchmark/scorer.d.ts +12 -0
- package/dist/src/benchmark/scorer.js +124 -0
- package/dist/src/benchmark/types.d.ts +54 -0
- package/dist/src/benchmark/types.js +1 -0
- package/dist/src/cache/deliberation-cache.d.ts +49 -0
- package/dist/src/cache/deliberation-cache.js +127 -0
- package/dist/src/cli/commands/config-cmd.d.ts +11 -0
- package/dist/src/cli/commands/config-cmd.js +190 -0
- package/dist/src/cli/commands/demo.d.ts +12 -0
- package/dist/src/cli/commands/demo.js +66 -0
- package/dist/src/cli/commands/setup.d.ts +7 -0
- package/dist/src/cli/commands/setup.js +182 -0
- package/dist/src/cli/i18n.d.ts +89 -0
- package/dist/src/cli/i18n.js +176 -0
- package/dist/src/cli/interactive-select.d.ts +27 -0
- package/dist/src/cli/interactive-select.js +130 -0
- package/dist/src/cli/tui-setup.d.ts +24 -0
- package/dist/src/cli/tui-setup.js +42 -0
- package/dist/src/config/cli-detector.d.ts +37 -0
- package/dist/src/config/cli-detector.js +99 -0
- package/dist/src/config/user-config.d.ts +81 -0
- package/dist/src/config/user-config.js +134 -0
- package/dist/src/context/auto-collector.d.ts +43 -0
- package/dist/src/context/auto-collector.js +337 -0
- package/dist/src/context/manager.d.ts +35 -0
- package/dist/src/context/manager.js +162 -0
- package/dist/src/context/serializer.d.ts +20 -0
- package/dist/src/context/serializer.js +52 -0
- package/dist/src/demo/recorded-deliberation.d.ts +13 -0
- package/dist/src/demo/recorded-deliberation.js +277 -0
- package/dist/src/engine/angel-detector.d.ts +83 -0
- package/dist/src/engine/angel-detector.js +334 -0
- package/dist/src/engine/at-field.d.ts +40 -0
- package/dist/src/engine/at-field.js +195 -0
- package/dist/src/engine/berserk-orchestrator.d.ts +66 -0
- package/dist/src/engine/berserk-orchestrator.js +378 -0
- package/dist/src/engine/change-metrics.d.ts +56 -0
- package/dist/src/engine/change-metrics.js +214 -0
- package/dist/src/engine/consensus.d.ts +20 -0
- package/dist/src/engine/consensus.js +146 -0
- package/dist/src/engine/dead-sea-scrolls.d.ts +132 -0
- package/dist/src/engine/dead-sea-scrolls.js +610 -0
- package/dist/src/engine/drift-detector.d.ts +39 -0
- package/dist/src/engine/drift-detector.js +225 -0
- package/dist/src/engine/dummy-plug.d.ts +44 -0
- package/dist/src/engine/dummy-plug.js +190 -0
- package/dist/src/engine/engram-manager.d.ts +55 -0
- package/dist/src/engine/engram-manager.js +306 -0
- package/dist/src/engine/events.d.ts +130 -0
- package/dist/src/engine/events.js +44 -0
- package/dist/src/engine/gospel.d.ts +30 -0
- package/dist/src/engine/gospel.js +129 -0
- package/dist/src/engine/hallucination-detector.d.ts +33 -0
- package/dist/src/engine/hallucination-detector.js +215 -0
- package/dist/src/engine/human-resolver.d.ts +19 -0
- package/dist/src/engine/human-resolver.js +89 -0
- package/dist/src/engine/instrumentality.d.ts +64 -0
- package/dist/src/engine/instrumentality.js +297 -0
- package/dist/src/engine/iruel-battle.d.ts +79 -0
- package/dist/src/engine/iruel-battle.js +319 -0
- package/dist/src/engine/kernel/deliberation-kernel.d.ts +12 -0
- package/dist/src/engine/kernel/deliberation-kernel.js +303 -0
- package/dist/src/engine/kernel/index.d.ts +8 -0
- package/dist/src/engine/kernel/index.js +7 -0
- package/dist/src/engine/kernel/phase-runner.d.ts +10 -0
- package/dist/src/engine/kernel/phase-runner.js +155 -0
- package/dist/src/engine/kernel/post-processor.d.ts +17 -0
- package/dist/src/engine/kernel/post-processor.js +131 -0
- package/dist/src/engine/kernel/types.d.ts +107 -0
- package/dist/src/engine/kernel/types.js +1 -0
- package/dist/src/engine/kernel/unit-executor.d.ts +6 -0
- package/dist/src/engine/kernel/unit-executor.js +132 -0
- package/dist/src/engine/lcl-manager.d.ts +44 -0
- package/dist/src/engine/lcl-manager.js +143 -0
- package/dist/src/engine/middleware/cache.d.ts +7 -0
- package/dist/src/engine/middleware/cache.js +29 -0
- package/dist/src/engine/middleware/chain.d.ts +18 -0
- package/dist/src/engine/middleware/chain.js +45 -0
- package/dist/src/engine/middleware/firewall.d.ts +8 -0
- package/dist/src/engine/middleware/firewall.js +24 -0
- package/dist/src/engine/middleware/index.d.ts +4 -0
- package/dist/src/engine/middleware/index.js +3 -0
- package/dist/src/engine/middleware/types.d.ts +43 -0
- package/dist/src/engine/middleware/types.js +1 -0
- package/dist/src/engine/nebuchadnezzar-key.d.ts +61 -0
- package/dist/src/engine/nebuchadnezzar-key.js +203 -0
- package/dist/src/engine/neon-genesis.d.ts +52 -0
- package/dist/src/engine/neon-genesis.js +203 -0
- package/dist/src/engine/objective-judge.d.ts +53 -0
- package/dist/src/engine/objective-judge.js +214 -0
- package/dist/src/engine/offline-mode.d.ts +18 -0
- package/dist/src/engine/offline-mode.js +46 -0
- package/dist/src/engine/orchestrator.d.ts +79 -0
- package/dist/src/engine/orchestrator.js +58 -0
- package/dist/src/engine/secret-cipher.d.ts +26 -0
- package/dist/src/engine/secret-cipher.js +114 -0
- package/dist/src/engine/seele-council.d.ts +90 -0
- package/dist/src/engine/seele-council.js +482 -0
- package/dist/src/engine/self-destruct.d.ts +61 -0
- package/dist/src/engine/self-destruct.js +231 -0
- package/dist/src/engine/self-evolution.d.ts +64 -0
- package/dist/src/engine/self-evolution.js +368 -0
- package/dist/src/engine/sync-rate.d.ts +45 -0
- package/dist/src/engine/sync-rate.js +151 -0
- package/dist/src/engine/type666-firewall.d.ts +76 -0
- package/dist/src/engine/type666-firewall.js +343 -0
- package/dist/src/engine/umbilical-cable.d.ts +41 -0
- package/dist/src/engine/umbilical-cable.js +192 -0
- package/dist/src/index.d.ts +106 -0
- package/dist/src/index.js +426 -0
- package/dist/src/mcp/server.d.ts +38 -0
- package/dist/src/mcp/server.js +196 -0
- package/dist/src/metrics/token-tracker.d.ts +38 -0
- package/dist/src/metrics/token-tracker.js +112 -0
- package/dist/src/parsers/json-extractor.d.ts +9 -0
- package/dist/src/parsers/json-extractor.js +239 -0
- package/dist/src/parsers/opinion-schema.d.ts +81 -0
- package/dist/src/parsers/opinion-schema.js +147 -0
- package/dist/src/parsers/unstructured-parser.d.ts +20 -0
- package/dist/src/parsers/unstructured-parser.js +122 -0
- package/dist/src/pipelines/architecture.d.ts +10 -0
- package/dist/src/pipelines/architecture.js +9 -0
- package/dist/src/pipelines/bug-analysis.d.ts +9 -0
- package/dist/src/pipelines/bug-analysis.js +8 -0
- package/dist/src/pipelines/code-review.d.ts +10 -0
- package/dist/src/pipelines/code-review.js +30 -0
- package/dist/src/pipelines/custom.d.ts +14 -0
- package/dist/src/pipelines/custom.js +29 -0
- package/dist/src/pipelines/registry.d.ts +9 -0
- package/dist/src/pipelines/registry.js +20 -0
- package/dist/src/prompts/personas.d.ts +6 -0
- package/dist/src/prompts/personas.js +44 -0
- package/dist/src/prompts/schemas.d.ts +4 -0
- package/dist/src/prompts/schemas.js +24 -0
- package/dist/src/prompts/templates.d.ts +6 -0
- package/dist/src/prompts/templates.js +91 -0
- package/dist/src/repl/accessibility.d.ts +23 -0
- package/dist/src/repl/accessibility.js +46 -0
- package/dist/src/repl/banner.d.ts +4 -0
- package/dist/src/repl/banner.js +28 -0
- package/dist/src/repl/boot-animation.d.ts +13 -0
- package/dist/src/repl/boot-animation.js +143 -0
- package/dist/src/repl/completer.d.ts +21 -0
- package/dist/src/repl/completer.js +168 -0
- package/dist/src/repl/context.d.ts +24 -0
- package/dist/src/repl/context.js +42 -0
- package/dist/src/repl/display-utils.d.ts +13 -0
- package/dist/src/repl/display-utils.js +65 -0
- package/dist/src/repl/event-listener.d.ts +18 -0
- package/dist/src/repl/event-listener.js +112 -0
- package/dist/src/repl/export-formatter.d.ts +8 -0
- package/dist/src/repl/export-formatter.js +73 -0
- package/dist/src/repl/ghost-text.d.ts +31 -0
- package/dist/src/repl/ghost-text.js +119 -0
- package/dist/src/repl/handoff-animation.d.ts +15 -0
- package/dist/src/repl/handoff-animation.js +65 -0
- package/dist/src/repl/history.d.ts +16 -0
- package/dist/src/repl/history.js +130 -0
- package/dist/src/repl/job-registry.d.ts +26 -0
- package/dist/src/repl/job-registry.js +80 -0
- package/dist/src/repl/magi-repl.d.ts +72 -0
- package/dist/src/repl/magi-repl.js +1008 -0
- package/dist/src/repl/multiline-input.d.ts +45 -0
- package/dist/src/repl/multiline-input.js +78 -0
- package/dist/src/repl/prompt-builder.d.ts +19 -0
- package/dist/src/repl/prompt-builder.js +36 -0
- package/dist/src/repl/repl-state.d.ts +5 -0
- package/dist/src/repl/repl-state.js +19 -0
- package/dist/src/repl/result-display.d.ts +8 -0
- package/dist/src/repl/result-display.js +195 -0
- package/dist/src/repl/session-stats.d.ts +26 -0
- package/dist/src/repl/session-stats.js +119 -0
- package/dist/src/repl/slash-commands.d.ts +60 -0
- package/dist/src/repl/slash-commands.js +725 -0
- package/dist/src/repl/terminal-sanitize.d.ts +14 -0
- package/dist/src/repl/terminal-sanitize.js +19 -0
- package/dist/src/reporters/console.d.ts +7 -0
- package/dist/src/reporters/console.js +78 -0
- package/dist/src/reporters/json.d.ts +2 -0
- package/dist/src/reporters/json.js +3 -0
- package/dist/src/reporters/markdown.d.ts +2 -0
- package/dist/src/reporters/markdown.js +65 -0
- package/dist/src/reporters/streaming.d.ts +20 -0
- package/dist/src/reporters/streaming.js +178 -0
- package/dist/src/tui/activity-log.d.ts +23 -0
- package/dist/src/tui/activity-log.js +67 -0
- package/dist/src/tui/animations.d.ts +39 -0
- package/dist/src/tui/animations.js +167 -0
- package/dist/src/tui/ansi.d.ts +28 -0
- package/dist/src/tui/ansi.js +51 -0
- package/dist/src/tui/boot-sequence.d.ts +11 -0
- package/dist/src/tui/boot-sequence.js +98 -0
- package/dist/src/tui/colors.d.ts +101 -0
- package/dist/src/tui/colors.js +71 -0
- package/dist/src/tui/header.d.ts +24 -0
- package/dist/src/tui/header.js +122 -0
- package/dist/src/tui/index.d.ts +3 -0
- package/dist/src/tui/index.js +3 -0
- package/dist/src/tui/keypress.d.ts +25 -0
- package/dist/src/tui/keypress.js +95 -0
- package/dist/src/tui/layout.d.ts +74 -0
- package/dist/src/tui/layout.js +171 -0
- package/dist/src/tui/magi-tui.d.ts +101 -0
- package/dist/src/tui/magi-tui.js +754 -0
- package/dist/src/tui/panel.d.ts +45 -0
- package/dist/src/tui/panel.js +292 -0
- package/dist/src/tui/screen-buffer.d.ts +54 -0
- package/dist/src/tui/screen-buffer.js +262 -0
- package/dist/src/tui/status-bar.d.ts +25 -0
- package/dist/src/tui/status-bar.js +124 -0
- package/dist/src/tui/terminal-detect.d.ts +26 -0
- package/dist/src/tui/terminal-detect.js +44 -0
- package/dist/src/tui/tui-helpers.d.ts +12 -0
- package/dist/src/tui/tui-helpers.js +37 -0
- package/dist/src/types/adapter.d.ts +75 -0
- package/dist/src/types/adapter.js +36 -0
- package/dist/src/types/config.d.ts +108 -0
- package/dist/src/types/config.js +85 -0
- package/dist/src/types/consensus.d.ts +55 -0
- package/dist/src/types/consensus.js +17 -0
- package/dist/src/types/core.d.ts +178 -0
- package/dist/src/types/core.js +85 -0
- package/dist/src/types/magi-api.d.ts +62 -0
- package/dist/src/types/magi-api.js +7 -0
- package/dist/src/types/phase-h.d.ts +142 -0
- package/dist/src/types/phase-h.js +7 -0
- package/dist/src/types/phase-i.d.ts +186 -0
- package/dist/src/types/phase-i.js +6 -0
- package/dist/src/types/phase-k.d.ts +259 -0
- package/dist/src/types/phase-k.js +6 -0
- package/dist/src/types/phase-l.d.ts +199 -0
- package/dist/src/types/phase-l.js +6 -0
- package/dist/src/types/pipeline.d.ts +37 -0
- package/dist/src/types/pipeline.js +2 -0
- package/dist/src/utils/abstain-factory.d.ts +2 -0
- package/dist/src/utils/abstain-factory.js +18 -0
- package/dist/src/utils/errors.d.ts +34 -0
- package/dist/src/utils/errors.js +59 -0
- package/dist/src/utils/file-validator.d.ts +50 -0
- package/dist/src/utils/file-validator.js +124 -0
- package/dist/src/utils/fire-and-forget.d.ts +5 -0
- package/dist/src/utils/fire-and-forget.js +10 -0
- package/dist/src/utils/flag-validator.d.ts +21 -0
- package/dist/src/utils/flag-validator.js +79 -0
- package/dist/src/utils/freeze.d.ts +8 -0
- package/dist/src/utils/freeze.js +16 -0
- package/dist/src/utils/language-detector.d.ts +16 -0
- package/dist/src/utils/language-detector.js +159 -0
- package/dist/src/utils/latency-tracker.d.ts +45 -0
- package/dist/src/utils/latency-tracker.js +100 -0
- package/dist/src/utils/logger.d.ts +33 -0
- package/dist/src/utils/logger.js +112 -0
- package/dist/src/utils/process.d.ts +40 -0
- package/dist/src/utils/process.js +253 -0
- package/dist/src/utils/retry.d.ts +12 -0
- package/dist/src/utils/retry.js +30 -0
- package/dist/src/utils/safe-fs.d.ts +38 -0
- package/dist/src/utils/safe-fs.js +56 -0
- package/dist/src/utils/safe-json-parse.d.ts +15 -0
- package/dist/src/utils/safe-json-parse.js +49 -0
- package/dist/src/utils/sanitize.d.ts +14 -0
- package/dist/src/utils/sanitize.js +186 -0
- package/dist/src/utils/semaphore.d.ts +22 -0
- package/dist/src/utils/semaphore.js +57 -0
- package/dist/src/utils/shutdown.d.ts +6 -0
- package/dist/src/utils/shutdown.js +51 -0
- package/dist/src/utils/tty.d.ts +5 -0
- package/dist/src/utils/tty.js +7 -0
- package/package.json +82 -0
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MagiRepl — Interactive REPL for the MAGI System.
|
|
3
|
+
*
|
|
4
|
+
* Provides a Claude Code-like experience: type questions naturally,
|
|
5
|
+
* context persists across turns, and the full-screen Evangelion TUI
|
|
6
|
+
* takes over during deliberation.
|
|
7
|
+
*
|
|
8
|
+
* Key design decision (Gemini review):
|
|
9
|
+
* readline is **close → recreate** per deliberation rather than
|
|
10
|
+
* pause/resume, to avoid stdin state conflicts with the TUI's raw mode.
|
|
11
|
+
*/
|
|
12
|
+
import { createInterface } from 'node:readline/promises';
|
|
13
|
+
import * as readline from 'node:readline';
|
|
14
|
+
import { randomBytes } from 'node:crypto';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { Magi } from '../index.js';
|
|
17
|
+
import { initializeTui } from '../cli/tui-setup.js';
|
|
18
|
+
import { CURSOR_SHOW, ALT_SCREEN_LEAVE, ERASE_LINE, ERASE_TO_END, moveUp } from '../tui/ansi.js';
|
|
19
|
+
import { EVA_PALETTE } from '../tui/colors.js';
|
|
20
|
+
import { logger } from '../utils/logger.js';
|
|
21
|
+
import { ReplContext } from './context.js';
|
|
22
|
+
import { createSlashCommands, parseInput, suggestCommand, } from './slash-commands.js';
|
|
23
|
+
import { JobRegistry } from './job-registry.js';
|
|
24
|
+
import { printBanner } from './banner.js';
|
|
25
|
+
import { assertTransition } from './repl-state.js';
|
|
26
|
+
import { buildPrompt, DEFAULT_SESSION_STATE } from './prompt-builder.js';
|
|
27
|
+
import { createReplHistory } from './history.js';
|
|
28
|
+
import { createCompleter } from './completer.js';
|
|
29
|
+
import { formatResultDisplay } from './result-display.js';
|
|
30
|
+
import { runReplBoot } from './boot-animation.js';
|
|
31
|
+
import { createReplEventListener } from './event-listener.js';
|
|
32
|
+
import { sanitizeForTerminal } from './terminal-sanitize.js';
|
|
33
|
+
import { estimateTokens } from '../metrics/token-tracker.js';
|
|
34
|
+
import { runHandoffAnimation } from './handoff-animation.js';
|
|
35
|
+
import { createMultilineCollector, isContinuationLine, joinContinuationLines, MAX_CONTINUATION_LINES } from './multiline-input.js';
|
|
36
|
+
import { createSessionStats, recordInput, recordSlashCommand, recordDeliberation, formatSessionSummary } from './session-stats.js';
|
|
37
|
+
import { shouldSkipAnimation } from './accessibility.js';
|
|
38
|
+
import { formatDeliberationAsJson, formatDeliberationAsMarkdown } from './export-formatter.js';
|
|
39
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
40
|
+
import { lstat } from 'node:fs/promises';
|
|
41
|
+
import { resolve, relative } from 'node:path';
|
|
42
|
+
import { safeWriteFile } from '../utils/safe-fs.js';
|
|
43
|
+
import { createGhostTextEngine, isGhostTextDisabled } from './ghost-text.js';
|
|
44
|
+
import { loadUserConfigSafe, mergeWithDefaults } from '../config/user-config.js';
|
|
45
|
+
const { frame, abstain, approve, warning } = EVA_PALETTE;
|
|
46
|
+
const FRAME_CHALK = chalk.rgb(frame.r, frame.g, frame.b);
|
|
47
|
+
const DIM = chalk.rgb(abstain.r, abstain.g, abstain.b);
|
|
48
|
+
const OK_C = chalk.rgb(approve.r, approve.g, approve.b);
|
|
49
|
+
const WARN_C = chalk.rgb(warning.r, warning.g, warning.b);
|
|
50
|
+
const TRANSITION = FRAME_CHALK;
|
|
51
|
+
const MAX_INPUT_LENGTH = 100_000;
|
|
52
|
+
const ENGRAM_CONTEXT_CAP = 5_000;
|
|
53
|
+
const MAX_READLINE_RETRIES = 3;
|
|
54
|
+
// Module-scoped: emitKeypressEvents should only be called once per process.stdin
|
|
55
|
+
let keypressInitialized = false;
|
|
56
|
+
function isConfirmationDispatch(result) {
|
|
57
|
+
return typeof result === 'object' && result !== null && 'confirm' in result;
|
|
58
|
+
}
|
|
59
|
+
function isInteractiveDispatch(result) {
|
|
60
|
+
return typeof result === 'object' && result !== null && 'interactive' in result;
|
|
61
|
+
}
|
|
62
|
+
export class MagiRepl {
|
|
63
|
+
magi;
|
|
64
|
+
context;
|
|
65
|
+
slashCommands;
|
|
66
|
+
soundEnabled = false;
|
|
67
|
+
state = 'INIT';
|
|
68
|
+
abortController;
|
|
69
|
+
sessionState = { ...DEFAULT_SESSION_STATE };
|
|
70
|
+
history;
|
|
71
|
+
completerFn;
|
|
72
|
+
shownEngramIds = new Set();
|
|
73
|
+
stats = createSessionStats();
|
|
74
|
+
multilineCollector = createMultilineCollector();
|
|
75
|
+
lastSigintTime = 0;
|
|
76
|
+
lastDeliberation;
|
|
77
|
+
options;
|
|
78
|
+
ghostEngine;
|
|
79
|
+
jobRegistry = new JobRegistry();
|
|
80
|
+
currentRl;
|
|
81
|
+
watchJobId;
|
|
82
|
+
watchPollIntervalMs;
|
|
83
|
+
constructor(options) {
|
|
84
|
+
this.options = options ?? {};
|
|
85
|
+
this.magi = new Magi({ logLevel: 'warn', ...this.options.magiConfig });
|
|
86
|
+
this.context = new ReplContext();
|
|
87
|
+
this.slashCommands = createSlashCommands();
|
|
88
|
+
this.history = createReplHistory(this.options.historyDir);
|
|
89
|
+
this.soundEnabled = this.options.soundEnabled ?? false;
|
|
90
|
+
// Build completer from slash command registry
|
|
91
|
+
const completableCommands = [];
|
|
92
|
+
for (const [name, cmd] of this.slashCommands) {
|
|
93
|
+
completableCommands.push({ name, argKind: cmd.argKind, aliases: cmd.aliases });
|
|
94
|
+
}
|
|
95
|
+
// Ghost text engine — inline dim gray suggestions
|
|
96
|
+
// (initialized before completer so it can be passed as suppression source)
|
|
97
|
+
const ghostSource = {
|
|
98
|
+
getSlashCommands: () => [...this.slashCommands.keys()].map(k => `/${k}`),
|
|
99
|
+
getHistory: () => this.history.getEntries(),
|
|
100
|
+
getDeliberationTitles: () => this.context.getHistory().map(e => e.title),
|
|
101
|
+
};
|
|
102
|
+
this.ghostEngine = createGhostTextEngine(ghostSource, {
|
|
103
|
+
enabled: !isGhostTextDisabled(),
|
|
104
|
+
});
|
|
105
|
+
this.completerFn = createCompleter(completableCommands, {
|
|
106
|
+
ghostEngine: this.ghostEngine,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
get currentState() {
|
|
110
|
+
return this.state;
|
|
111
|
+
}
|
|
112
|
+
get running() {
|
|
113
|
+
return this.state !== 'DISPOSING';
|
|
114
|
+
}
|
|
115
|
+
transition(to) {
|
|
116
|
+
const from = this.state;
|
|
117
|
+
assertTransition(from, to);
|
|
118
|
+
this.state = to;
|
|
119
|
+
logger.debug('REPL state transition', { from, to });
|
|
120
|
+
}
|
|
121
|
+
async start() {
|
|
122
|
+
this.transition('READY');
|
|
123
|
+
// First-run: auto-launch setup if no config exists
|
|
124
|
+
const existingConfig = await loadUserConfigSafe();
|
|
125
|
+
if (!existingConfig) {
|
|
126
|
+
const { runSetup } = await import('../cli/commands/setup.js');
|
|
127
|
+
await runSetup();
|
|
128
|
+
// Rebuild Magi instance with the newly saved config
|
|
129
|
+
const freshConfig = await loadUserConfigSafe();
|
|
130
|
+
if (freshConfig) {
|
|
131
|
+
const merged = mergeWithDefaults(freshConfig);
|
|
132
|
+
this.magi = new Magi({ logLevel: 'warn', ...merged });
|
|
133
|
+
this.soundEnabled = freshConfig.tui?.soundEnabled ?? false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Boot animation (replaces static banner)
|
|
137
|
+
if (!this.options.skipBootAnimation) {
|
|
138
|
+
await runReplBoot({ write: s => process.stdout.write(s) });
|
|
139
|
+
}
|
|
140
|
+
// Load history for readline
|
|
141
|
+
const historyEntries = await this.history.load();
|
|
142
|
+
// Startup health check (non-blocking, updates prompt)
|
|
143
|
+
this.checkUnitsOnStartup();
|
|
144
|
+
while (this.running) {
|
|
145
|
+
const pendingTask = await this.readInputLoop(historyEntries);
|
|
146
|
+
if (!pendingTask)
|
|
147
|
+
break; // quit or EOF
|
|
148
|
+
if (pendingTask === 'continue')
|
|
149
|
+
continue;
|
|
150
|
+
await this.runDeliberation(pendingTask.task, pendingTask.berserk);
|
|
151
|
+
}
|
|
152
|
+
await this.dispose();
|
|
153
|
+
}
|
|
154
|
+
// ── Input loop ──────────────────────────────────────────────
|
|
155
|
+
async readInputLoop(historyEntries, retryCount = 0) {
|
|
156
|
+
this.transition('READING_INPUT');
|
|
157
|
+
let rl;
|
|
158
|
+
try {
|
|
159
|
+
rl = this.createReadline(historyEntries);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
logger.debug('readInputLoop: createReadline failed', { error: String(error) });
|
|
163
|
+
console.error(chalk.red(`\n Failed to create readline: ${String(error)}\n`));
|
|
164
|
+
this.sessionState.hasError = true;
|
|
165
|
+
if (this.running && retryCount < MAX_READLINE_RETRIES) {
|
|
166
|
+
return this.readInputLoop(historyEntries, retryCount + 1);
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
// Continuation line buffer (for trailing-backslash mode)
|
|
171
|
+
let continuationBuffer = [];
|
|
172
|
+
try {
|
|
173
|
+
for await (const line of rl) {
|
|
174
|
+
const trimmed = line.trim();
|
|
175
|
+
// ── Multiline collecting mode (/paste) ──
|
|
176
|
+
if (this.state === 'MULTILINE_COLLECTING') {
|
|
177
|
+
const res = this.multilineCollector.feed(line);
|
|
178
|
+
if (res.status === 'collecting') {
|
|
179
|
+
rl.setPrompt(DIM('... '));
|
|
180
|
+
rl.prompt();
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
this.multilineCollector.reset();
|
|
184
|
+
this.transition('READING_INPUT');
|
|
185
|
+
if (res.status === 'aborted') {
|
|
186
|
+
console.log(DIM(' Multiline input aborted.'));
|
|
187
|
+
rl.setPrompt(buildPrompt(this.sessionState));
|
|
188
|
+
rl.prompt();
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// res.status === 'complete'
|
|
192
|
+
if (!res.text.trim()) {
|
|
193
|
+
rl.setPrompt(buildPrompt(this.sessionState));
|
|
194
|
+
rl.prompt();
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// Treat collected text as deliberation input
|
|
198
|
+
const result = await this.handleInput(res.text.trim());
|
|
199
|
+
if (!result || result === 'paste') {
|
|
200
|
+
this.transition('READING_INPUT');
|
|
201
|
+
rl.setPrompt(buildPrompt(this.sessionState));
|
|
202
|
+
rl.prompt();
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (result === 'quit') {
|
|
206
|
+
rl.close();
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
if (isConfirmationDispatch(result)) {
|
|
210
|
+
rl.close();
|
|
211
|
+
await this.handleConfirmation(result.confirm);
|
|
212
|
+
return 'continue';
|
|
213
|
+
}
|
|
214
|
+
if (isInteractiveDispatch(result)) {
|
|
215
|
+
rl.close();
|
|
216
|
+
try {
|
|
217
|
+
await result.interactive();
|
|
218
|
+
}
|
|
219
|
+
catch { /* handled inline */ }
|
|
220
|
+
return 'continue';
|
|
221
|
+
}
|
|
222
|
+
rl.close();
|
|
223
|
+
this.transition('READY');
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
// ── Continuation line mode (trailing backslash) ──
|
|
227
|
+
if (continuationBuffer.length > 0) {
|
|
228
|
+
continuationBuffer.push(line);
|
|
229
|
+
if (continuationBuffer.length > MAX_CONTINUATION_LINES) {
|
|
230
|
+
console.log(chalk.yellow(`\n Continuation too long (max ${MAX_CONTINUATION_LINES} lines). Input discarded.\n`));
|
|
231
|
+
continuationBuffer = [];
|
|
232
|
+
rl.setPrompt(buildPrompt(this.sessionState));
|
|
233
|
+
rl.prompt();
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!isContinuationLine(trimmed)) {
|
|
237
|
+
// Last line — join and process
|
|
238
|
+
const joined = joinContinuationLines(continuationBuffer);
|
|
239
|
+
continuationBuffer = [];
|
|
240
|
+
this.transition('READING_INPUT');
|
|
241
|
+
const result = await this.handleInput(joined.trim());
|
|
242
|
+
if (!result || result === 'paste') {
|
|
243
|
+
this.transition('READING_INPUT');
|
|
244
|
+
rl.setPrompt(buildPrompt(this.sessionState));
|
|
245
|
+
rl.prompt();
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (result === 'quit') {
|
|
249
|
+
rl.close();
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
if (isConfirmationDispatch(result)) {
|
|
253
|
+
rl.close();
|
|
254
|
+
await this.handleConfirmation(result.confirm);
|
|
255
|
+
return 'continue';
|
|
256
|
+
}
|
|
257
|
+
if (isInteractiveDispatch(result)) {
|
|
258
|
+
rl.close();
|
|
259
|
+
try {
|
|
260
|
+
await result.interactive();
|
|
261
|
+
}
|
|
262
|
+
catch { /* handled inline */ }
|
|
263
|
+
return 'continue';
|
|
264
|
+
}
|
|
265
|
+
rl.close();
|
|
266
|
+
this.transition('READY');
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
// Still continuing
|
|
270
|
+
rl.setPrompt(DIM('... '));
|
|
271
|
+
rl.prompt();
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (!trimmed)
|
|
275
|
+
continue;
|
|
276
|
+
if (trimmed.length > MAX_INPUT_LENGTH) {
|
|
277
|
+
console.log(chalk.yellow(`\n Input too long (${trimmed.length.toLocaleString()} chars, max ${MAX_INPUT_LENGTH.toLocaleString()}). Please shorten.\n`));
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// Check for continuation line
|
|
281
|
+
if (isContinuationLine(trimmed)) {
|
|
282
|
+
continuationBuffer = [line];
|
|
283
|
+
rl.setPrompt(DIM('... '));
|
|
284
|
+
rl.prompt();
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Record to persistent history
|
|
288
|
+
this.history.addEntry(trimmed);
|
|
289
|
+
// Reset error state on valid input
|
|
290
|
+
this.sessionState.hasError = false;
|
|
291
|
+
const result = await this.handleInput(trimmed);
|
|
292
|
+
if (!result) {
|
|
293
|
+
// Handled inline, keep reading
|
|
294
|
+
this.transition('READING_INPUT');
|
|
295
|
+
rl.setPrompt(buildPrompt(this.sessionState));
|
|
296
|
+
rl.prompt();
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (result === 'quit') {
|
|
300
|
+
rl.close();
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
if (result === 'paste') {
|
|
304
|
+
// Enter multiline collecting mode
|
|
305
|
+
this.multilineCollector.reset();
|
|
306
|
+
this.transition('MULTILINE_COLLECTING');
|
|
307
|
+
console.log(DIM(' Paste your text. End with "." on its own line. /abort to cancel.'));
|
|
308
|
+
rl.setPrompt(DIM('... '));
|
|
309
|
+
rl.prompt();
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (isConfirmationDispatch(result)) {
|
|
313
|
+
rl.close();
|
|
314
|
+
await this.handleConfirmation(result.confirm);
|
|
315
|
+
return 'continue';
|
|
316
|
+
}
|
|
317
|
+
if (isInteractiveDispatch(result)) {
|
|
318
|
+
// Close readline to release stdin for the interactive UI
|
|
319
|
+
rl.close();
|
|
320
|
+
try {
|
|
321
|
+
await result.interactive();
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
console.error(WARN_C(` コマンド実行異常: ${String(error)}`));
|
|
325
|
+
this.sessionState.hasError = true;
|
|
326
|
+
}
|
|
327
|
+
return 'continue'; // readInputLoop will be re-entered with fresh readline
|
|
328
|
+
}
|
|
329
|
+
// Deliberation requested — close readline and return task
|
|
330
|
+
rl.close();
|
|
331
|
+
this.transition('READY');
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
const isNormalClose = error instanceof Error &&
|
|
337
|
+
'code' in error && error.code === 'ERR_USE_AFTER_CLOSE';
|
|
338
|
+
if (!isNormalClose) {
|
|
339
|
+
logger.debug('readInputLoop: readline error', { error: String(error) });
|
|
340
|
+
console.error(chalk.red(`\n Readline error: ${String(error)}\n`));
|
|
341
|
+
this.sessionState.hasError = true;
|
|
342
|
+
// Recover: recreate readline with retry limit
|
|
343
|
+
if (this.running && retryCount < MAX_READLINE_RETRIES) {
|
|
344
|
+
try {
|
|
345
|
+
rl.close();
|
|
346
|
+
}
|
|
347
|
+
catch { /* best-effort */ }
|
|
348
|
+
return this.readInputLoop(historyEntries, retryCount + 1);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// EOF (Ctrl+D)
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
createReadline(historyEntries) {
|
|
356
|
+
const prompt = buildPrompt(this.sessionState);
|
|
357
|
+
// P0-3: autoClose: false prevents stdin from being destroyed on rl.close(),
|
|
358
|
+
// allowing readline to be recreated per deliberation cycle.
|
|
359
|
+
const rl = createInterface({
|
|
360
|
+
input: process.stdin,
|
|
361
|
+
output: process.stdout,
|
|
362
|
+
terminal: process.stdin.isTTY ?? false,
|
|
363
|
+
prompt,
|
|
364
|
+
completer: this.completerFn,
|
|
365
|
+
history: historyEntries,
|
|
366
|
+
historySize: 1000,
|
|
367
|
+
removeHistoryDuplicates: true,
|
|
368
|
+
autoClose: false,
|
|
369
|
+
});
|
|
370
|
+
rl.on('SIGINT', () => {
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
if (now - this.lastSigintTime < 1500) {
|
|
373
|
+
// Double-tap → graceful exit
|
|
374
|
+
console.log('');
|
|
375
|
+
rl.close();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this.lastSigintTime = now;
|
|
379
|
+
rl.write('', { ctrl: true, name: 'u' });
|
|
380
|
+
console.log('');
|
|
381
|
+
console.log(DIM(' (Press Ctrl+C again to exit)'));
|
|
382
|
+
rl.setPrompt(buildPrompt(this.sessionState));
|
|
383
|
+
rl.prompt();
|
|
384
|
+
});
|
|
385
|
+
// Ghost text: keypress-driven inline suggestions (TTY only)
|
|
386
|
+
if (process.stdin.isTTY && !isGhostTextDisabled()) {
|
|
387
|
+
if (!keypressInitialized) {
|
|
388
|
+
emitKeypressEvents(process.stdin);
|
|
389
|
+
keypressInitialized = true;
|
|
390
|
+
}
|
|
391
|
+
let rlClosed = false;
|
|
392
|
+
const ghostHandler = (_str, key) => {
|
|
393
|
+
if (!key || rlClosed)
|
|
394
|
+
return;
|
|
395
|
+
// Tab → accept ghost suggestion
|
|
396
|
+
if (key.name === 'tab' && !key.shift) {
|
|
397
|
+
const accepted = this.ghostEngine.accept();
|
|
398
|
+
if (accepted) {
|
|
399
|
+
rl.write(accepted);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// After any other key, update ghost text on next tick
|
|
404
|
+
// Uses Node.js internal `rl.line` (stable since v0.x, not in public API)
|
|
405
|
+
setImmediate(() => {
|
|
406
|
+
if (rlClosed)
|
|
407
|
+
return;
|
|
408
|
+
const line = rl.line ?? '';
|
|
409
|
+
const cursor = rl.cursor ?? line.length;
|
|
410
|
+
const promptText = buildPrompt(this.sessionState);
|
|
411
|
+
this.ghostEngine.clear(s => process.stdout.write(s));
|
|
412
|
+
if (cursor !== line.length) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const suggestion = this.ghostEngine.suggest(line);
|
|
416
|
+
this.ghostEngine.render(line, promptText, suggestion, s => process.stdout.write(s));
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
process.stdin.on('keypress', ghostHandler);
|
|
420
|
+
// Cleanup on readline close — prevents post-close ghost writes
|
|
421
|
+
rl.once('close', () => {
|
|
422
|
+
rlClosed = true;
|
|
423
|
+
process.stdin.removeListener('keypress', ghostHandler);
|
|
424
|
+
this.ghostEngine.clear(s => process.stdout.write(s));
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
this.currentRl = rl;
|
|
428
|
+
rl.once('close', () => { this.currentRl = undefined; });
|
|
429
|
+
rl.prompt();
|
|
430
|
+
return rl;
|
|
431
|
+
}
|
|
432
|
+
// ── Input dispatch ────────────────────────────────────────
|
|
433
|
+
async handleInput(input) {
|
|
434
|
+
this.transition('DISPATCHING');
|
|
435
|
+
recordInput(this.stats);
|
|
436
|
+
const parsed = parseInput(input);
|
|
437
|
+
if (parsed.isSlash) {
|
|
438
|
+
return this.handleSlashCommand(parsed.command, parsed.args);
|
|
439
|
+
}
|
|
440
|
+
// Natural language → deliberation
|
|
441
|
+
const task = {
|
|
442
|
+
type: 'custom',
|
|
443
|
+
title: input.slice(0, 80),
|
|
444
|
+
description: input,
|
|
445
|
+
context: this.context.buildContextString(),
|
|
446
|
+
};
|
|
447
|
+
// EngRam: inject related past deliberations
|
|
448
|
+
this.injectEngramContext(task);
|
|
449
|
+
return { task, berserk: false };
|
|
450
|
+
}
|
|
451
|
+
async handleSlashCommand(command, args) {
|
|
452
|
+
const cmd = this.slashCommands.get(command);
|
|
453
|
+
if (!cmd) {
|
|
454
|
+
const suggestion = suggestCommand(command, this.slashCommands.keys());
|
|
455
|
+
const hint = suggestion ? ` Did you mean /${suggestion}?` : '';
|
|
456
|
+
console.log(WARN_C(` 未定義のコマンドパターン: /${command}.${hint} Type /help for available commands.`));
|
|
457
|
+
this.sessionState.hasError = true;
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
recordSlashCommand(this.stats);
|
|
461
|
+
let result;
|
|
462
|
+
try {
|
|
463
|
+
result = await cmd.execute(args, this.magi, this.context);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
console.error(WARN_C(` コマンド実行異常: ${String(error)}`));
|
|
467
|
+
this.sessionState.hasError = true;
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
if (result.output) {
|
|
471
|
+
console.log(result.output);
|
|
472
|
+
}
|
|
473
|
+
if (result.action === 'quit') {
|
|
474
|
+
return 'quit';
|
|
475
|
+
}
|
|
476
|
+
if (result.action === 'paste') {
|
|
477
|
+
return 'paste';
|
|
478
|
+
}
|
|
479
|
+
if (result.action === 'clear') {
|
|
480
|
+
console.clear();
|
|
481
|
+
printBanner();
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
if (result.action === 'reset') {
|
|
485
|
+
this.context.clear();
|
|
486
|
+
this.sessionState.contextEntryCount = 0;
|
|
487
|
+
console.log(DIM(' Context reset. Deliberation history cleared.'));
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
if (result.action === 'toggle-sound') {
|
|
491
|
+
this.soundEnabled = !this.soundEnabled;
|
|
492
|
+
const status = this.soundEnabled ? OK_C('ON ♪') : DIM('OFF');
|
|
493
|
+
console.log(` ${FRAME_CHALK('SOUND:')} ${status}`);
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
if (result.action === 'export') {
|
|
497
|
+
await this.exportDeliberation(result.output || '');
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
if (result.action === 'jobs') {
|
|
501
|
+
this.handleJobsCommand(result.output || '');
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
if (result.watch) {
|
|
505
|
+
this.handleWatchCommand(result.watch);
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
// Handle interactive commands (need exclusive stdin access)
|
|
509
|
+
// Return to readInputLoop which will close readline before executing.
|
|
510
|
+
if (result.interactive) {
|
|
511
|
+
return { interactive: result.interactive };
|
|
512
|
+
}
|
|
513
|
+
// Handle background job request
|
|
514
|
+
if (result.background) {
|
|
515
|
+
const bg = result.background;
|
|
516
|
+
try {
|
|
517
|
+
this.jobRegistry.add(bg.name, async (signal) => {
|
|
518
|
+
await bg.run(signal);
|
|
519
|
+
}, {
|
|
520
|
+
onSettled: (job) => {
|
|
521
|
+
this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
|
|
522
|
+
if (job.status === 'failed' && job.error) {
|
|
523
|
+
this.writeAbovePrompt(WARN_C(` Job "${bg.name}" failed: ${job.error}`));
|
|
524
|
+
}
|
|
525
|
+
else if (!job.abort.signal.aborted) {
|
|
526
|
+
this.writeAbovePrompt(DIM(` Job "${bg.name}" completed.`));
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
this.sessionState.hasError = true;
|
|
534
|
+
console.error(WARN_C(` コマンド実行異常: ${String(error)}`));
|
|
535
|
+
}
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
// Handle confirmation request (dangerous operations)
|
|
539
|
+
if (result.confirm) {
|
|
540
|
+
return { confirm: result.confirm };
|
|
541
|
+
}
|
|
542
|
+
if (result.deliberation) {
|
|
543
|
+
// Merge auto-collected context with REPL session context
|
|
544
|
+
const sessionContext = this.context.buildContextString();
|
|
545
|
+
const autoContext = result.deliberation.task.context;
|
|
546
|
+
let mergedContext;
|
|
547
|
+
if (sessionContext && autoContext) {
|
|
548
|
+
mergedContext = `${autoContext}\n\n${sessionContext}`;
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
mergedContext = autoContext ?? sessionContext;
|
|
552
|
+
}
|
|
553
|
+
const task = {
|
|
554
|
+
...result.deliberation.task,
|
|
555
|
+
context: mergedContext,
|
|
556
|
+
};
|
|
557
|
+
// EngRam: inject related past deliberations
|
|
558
|
+
this.injectEngramContext(task);
|
|
559
|
+
return {
|
|
560
|
+
task,
|
|
561
|
+
berserk: result.deliberation.berserk ?? false,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
// ── Confirmation flow (dangerous operations) ────────────────
|
|
567
|
+
async handleConfirmation(confirm) {
|
|
568
|
+
this.transition('CONFIRMING');
|
|
569
|
+
console.log(confirm.description);
|
|
570
|
+
const required = confirm.requiredInput ?? this.createConfirmationToken();
|
|
571
|
+
const timeoutMs = confirm.timeoutMs ?? 30_000;
|
|
572
|
+
try {
|
|
573
|
+
await this.drainPendingInput();
|
|
574
|
+
const rl = createInterface({
|
|
575
|
+
input: process.stdin,
|
|
576
|
+
output: process.stdout,
|
|
577
|
+
terminal: process.stdin.isTTY ?? false,
|
|
578
|
+
autoClose: false,
|
|
579
|
+
});
|
|
580
|
+
const ac = new AbortController();
|
|
581
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
582
|
+
let answer;
|
|
583
|
+
try {
|
|
584
|
+
answer = await rl.question(DIM(` 続行するには "${required}" と入力してください (${Math.round(timeoutMs / 1000)}秒でタイムアウト): `), { signal: ac.signal });
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
if (ac.signal.aborted || (error instanceof Error && error.name === 'AbortError')) {
|
|
588
|
+
console.log(WARN_C('\n タイムアウト — 操作を中止しました。'));
|
|
589
|
+
}
|
|
590
|
+
else if (error instanceof Error && (error.message.includes('readline was closed') || error.code === 'ERR_USE_AFTER_CLOSE')) {
|
|
591
|
+
console.log(WARN_C('\n 操作を中止しました。'));
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
this.sessionState.hasError = true;
|
|
595
|
+
console.error(WARN_C(` 確認入力異常: ${String(error)}`));
|
|
596
|
+
}
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
finally {
|
|
600
|
+
clearTimeout(timer);
|
|
601
|
+
rl.close();
|
|
602
|
+
}
|
|
603
|
+
if (answer.trim() !== required) {
|
|
604
|
+
console.log(DIM(' 操作を中止しました。'));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
this.transition('DISPATCHING');
|
|
608
|
+
try {
|
|
609
|
+
const result = await confirm.execute();
|
|
610
|
+
if (result.output) {
|
|
611
|
+
console.log(result.output);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
this.sessionState.hasError = true;
|
|
616
|
+
console.error(WARN_C(` 操作実行異常: ${String(error)}`));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
this.sessionState.hasError = true;
|
|
621
|
+
logger.debug('Confirmation flow error', { error: String(error) });
|
|
622
|
+
console.error(WARN_C(` 確認フロー異常: ${String(error)}`));
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
// Return to READY so the next loop can recreate readline cleanly.
|
|
626
|
+
if (this.state === 'CONFIRMING' || this.state === 'DISPATCHING') {
|
|
627
|
+
this.transition('READY');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// ── Background job management ──────────────────────────────
|
|
632
|
+
async drainPendingInput() {
|
|
633
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
634
|
+
if (!process.stdin.readable)
|
|
635
|
+
return;
|
|
636
|
+
while (process.stdin.read() !== null) {
|
|
637
|
+
// Discard buffered input so confirmation requires a new explicit entry.
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
createConfirmationToken() {
|
|
641
|
+
return `yes-${randomBytes(2).toString('hex')}`;
|
|
642
|
+
}
|
|
643
|
+
handleWatchCommand(command) {
|
|
644
|
+
if (command.command === 'status') {
|
|
645
|
+
const activeJob = this.watchJobId === undefined ? undefined : this.jobRegistry.get(this.watchJobId);
|
|
646
|
+
if (!activeJob || activeJob.status !== 'running') {
|
|
647
|
+
console.log(DIM(' Angel watch is stopped.'));
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const intervalLabel = this.watchPollIntervalMs ? ` (${this.watchPollIntervalMs}ms)` : '';
|
|
651
|
+
console.log(OK_C(` Angel watch is running as job #${activeJob.id}${intervalLabel}.`));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (command.command === 'stop') {
|
|
655
|
+
const activeJob = this.watchJobId === undefined ? undefined : this.jobRegistry.get(this.watchJobId);
|
|
656
|
+
if (!activeJob || activeJob.status !== 'running') {
|
|
657
|
+
console.log(DIM(' Angel watch is not running.'));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
this.magi.runtime.stopWatch();
|
|
661
|
+
this.jobRegistry.cancel(activeJob.id);
|
|
662
|
+
this.watchJobId = undefined;
|
|
663
|
+
this.watchPollIntervalMs = undefined;
|
|
664
|
+
this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
|
|
665
|
+
console.log(DIM(' Angel watch stopped.'));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const activeJob = this.watchJobId === undefined ? undefined : this.jobRegistry.get(this.watchJobId);
|
|
669
|
+
if (activeJob && activeJob.status === 'running') {
|
|
670
|
+
console.log(WARN_C(` Angel watch is already running as job #${activeJob.id}.`));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const job = this.jobRegistry.add('Angel Watch', async (signal) => {
|
|
675
|
+
this.magi.runtime.startWatch({ pollIntervalMs: command.pollIntervalMs, signal });
|
|
676
|
+
await new Promise((resolve) => {
|
|
677
|
+
signal.addEventListener('abort', () => resolve(), { once: true });
|
|
678
|
+
});
|
|
679
|
+
}, {
|
|
680
|
+
onSettled: (settledJob) => {
|
|
681
|
+
if (this.watchJobId === settledJob.id) {
|
|
682
|
+
this.watchJobId = undefined;
|
|
683
|
+
this.watchPollIntervalMs = undefined;
|
|
684
|
+
}
|
|
685
|
+
this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
|
|
686
|
+
if (settledJob.status === 'failed' && settledJob.error) {
|
|
687
|
+
this.writeAbovePrompt(WARN_C(` Job "${settledJob.name}" failed: ${settledJob.error}`));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (!settledJob.abort.signal.aborted) {
|
|
691
|
+
this.writeAbovePrompt(DIM(` Job "${settledJob.name}" completed.`));
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
this.watchJobId = job.id;
|
|
696
|
+
this.watchPollIntervalMs = command.pollIntervalMs;
|
|
697
|
+
this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
|
|
698
|
+
console.log(OK_C(` Angel watch started as job #${job.id}${command.pollIntervalMs ? ` (${command.pollIntervalMs}ms)` : ''}.`));
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
this.sessionState.hasError = true;
|
|
702
|
+
console.error(WARN_C(` Angel watch failed: ${String(error)}`));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
handleJobsCommand(args) {
|
|
706
|
+
const parts = args.trim().split(/\s+/);
|
|
707
|
+
if (parts[0] === 'cancel' && parts[1]) {
|
|
708
|
+
const id = parseInt(parts[1], 10);
|
|
709
|
+
if (Number.isNaN(id)) {
|
|
710
|
+
console.log(WARN_C(' Usage: /jobs cancel <id>'));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const ok = this.jobRegistry.cancel(id);
|
|
714
|
+
if (ok) {
|
|
715
|
+
if (this.watchJobId === id) {
|
|
716
|
+
this.magi.runtime.stopWatch();
|
|
717
|
+
this.watchJobId = undefined;
|
|
718
|
+
this.watchPollIntervalMs = undefined;
|
|
719
|
+
}
|
|
720
|
+
this.sessionState.backgroundJobCount = this.jobRegistry.getRunningCount();
|
|
721
|
+
console.log(OK_C(` Job #${id} cancelled.`));
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
console.log(WARN_C(` Job #${id} not found or already finished.`));
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const jobs = this.jobRegistry.list();
|
|
729
|
+
if (jobs.length === 0) {
|
|
730
|
+
console.log(DIM(' No background jobs.'));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const lines = ['', FRAME_CHALK.bold(' Background Jobs:'), ''];
|
|
734
|
+
for (const job of jobs) {
|
|
735
|
+
const elapsed = Math.round((Date.now() - job.startedAt) / 1000);
|
|
736
|
+
const statusColor = job.status === 'running' ? OK_C
|
|
737
|
+
: job.status === 'failed' ? WARN_C : DIM;
|
|
738
|
+
lines.push(` #${job.id} ${statusColor(job.status.toUpperCase().padEnd(7))} ${job.name} (${elapsed}s)`);
|
|
739
|
+
if (job.error)
|
|
740
|
+
lines.push(` ${WARN_C(job.error)}`);
|
|
741
|
+
}
|
|
742
|
+
lines.push('');
|
|
743
|
+
console.log(lines.join('\n'));
|
|
744
|
+
}
|
|
745
|
+
writeAbovePrompt(message) {
|
|
746
|
+
if (!this.currentRl)
|
|
747
|
+
return;
|
|
748
|
+
const output = process.stdout;
|
|
749
|
+
readline.clearLine(output, 0);
|
|
750
|
+
readline.cursorTo(output, 0);
|
|
751
|
+
output.write(message + '\n');
|
|
752
|
+
this.currentRl.prompt(true);
|
|
753
|
+
}
|
|
754
|
+
// ── EngRam integration ──────────────────────────────────────
|
|
755
|
+
injectEngramContext(task) {
|
|
756
|
+
try {
|
|
757
|
+
const engram = this.magi.getEngramManager();
|
|
758
|
+
const query = `${task.title} ${task.description}`;
|
|
759
|
+
const similar = engram.findSimilar(query, 3);
|
|
760
|
+
// Deduplicate: filter out entries already in ReplContext or shown this session
|
|
761
|
+
const contextIds = new Set(this.context.getHistory().map(e => e.id));
|
|
762
|
+
const filtered = similar.filter(m => !contextIds.has(m.deliberationId) && !this.shownEngramIds.has(m.deliberationId));
|
|
763
|
+
if (filtered.length === 0)
|
|
764
|
+
return;
|
|
765
|
+
// Track shown IDs at session level to avoid re-injection
|
|
766
|
+
for (const m of filtered) {
|
|
767
|
+
this.shownEngramIds.add(m.deliberationId);
|
|
768
|
+
}
|
|
769
|
+
// Show related deliberations (sanitize external data)
|
|
770
|
+
console.log(DIM(' Related deliberations:'));
|
|
771
|
+
for (const m of filtered) {
|
|
772
|
+
console.log(DIM(` ⬢ ${sanitizeForTerminal(m.taskTitle)} (${m.decision})`));
|
|
773
|
+
}
|
|
774
|
+
// Inject into context (capped)
|
|
775
|
+
const engramContext = filtered
|
|
776
|
+
.map(m => `[Past] "${m.taskTitle}" → ${m.decision}: ${(m.keyPoints || []).slice(0, 2).join(', ')}`)
|
|
777
|
+
.join('\n')
|
|
778
|
+
.slice(0, ENGRAM_CONTEXT_CAP);
|
|
779
|
+
task.context = [task.context, engramContext].filter(Boolean).join('\n\n');
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
// EngRam is optional — graceful degradation
|
|
783
|
+
logger.debug('EngRam context injection failed');
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// ── Deliberation execution ────────────────────────────────
|
|
787
|
+
async runDeliberation(task, berserk) {
|
|
788
|
+
this.transition('DELIBERATING');
|
|
789
|
+
// Update session state
|
|
790
|
+
this.sessionState.berserkActive = berserk;
|
|
791
|
+
let handoffLineCount;
|
|
792
|
+
// Handoff animation before TUI takes over
|
|
793
|
+
if (shouldSkipAnimation()) {
|
|
794
|
+
console.log('');
|
|
795
|
+
console.log(TRANSITION(' DELIBERATION SEQUENCE — 思考回路 展開'));
|
|
796
|
+
console.log('');
|
|
797
|
+
handoffLineCount = 3;
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
handoffLineCount = await runHandoffAnimation({
|
|
801
|
+
write: s => process.stdout.write(s),
|
|
802
|
+
taskTitle: task.title,
|
|
803
|
+
berserk,
|
|
804
|
+
unitCount: this.sessionState.totalUnits,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
this.abortController = new AbortController();
|
|
808
|
+
const { signal } = this.abortController;
|
|
809
|
+
// Ctrl+C during deliberation → abort instead of default behavior
|
|
810
|
+
const cancelHandler = () => { this.abortController?.abort(); };
|
|
811
|
+
process.once('SIGINT', cancelHandler);
|
|
812
|
+
let tuiInstance;
|
|
813
|
+
let eventListener;
|
|
814
|
+
let disposeStreaming;
|
|
815
|
+
try {
|
|
816
|
+
const setup = await initializeTui(this.magi.getEventBus(), this.options.tuiEnabled ?? true, { soundEnabled: this.soundEnabled });
|
|
817
|
+
tuiInstance = setup.tuiInstance;
|
|
818
|
+
disposeStreaming = setup.disposeStreaming;
|
|
819
|
+
// If TUI is not available AND no streaming reporter active, use inline listener
|
|
820
|
+
if (!tuiInstance && !setup.streamingActive) {
|
|
821
|
+
eventListener = createReplEventListener({
|
|
822
|
+
eventBus: this.magi.getEventBus(),
|
|
823
|
+
write: s => process.stdout.write(s),
|
|
824
|
+
});
|
|
825
|
+
eventListener.subscribe();
|
|
826
|
+
}
|
|
827
|
+
let result;
|
|
828
|
+
if (berserk) {
|
|
829
|
+
result = await this.magi.deliberateBerserk(task, { signal });
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
result = await this.magi.deliberate(task, { signal });
|
|
833
|
+
}
|
|
834
|
+
if (tuiInstance)
|
|
835
|
+
await tuiInstance.waitForDismiss();
|
|
836
|
+
tuiInstance?.dispose();
|
|
837
|
+
tuiInstance = undefined;
|
|
838
|
+
this.transition('SHOWING_RESULT');
|
|
839
|
+
if (handoffLineCount > 0) {
|
|
840
|
+
process.stdout.write(this.clearRecentLines(handoffLineCount));
|
|
841
|
+
}
|
|
842
|
+
process.stdout.write(ERASE_TO_END);
|
|
843
|
+
// Rich result display (replaces old printResultSummary)
|
|
844
|
+
console.log(formatResultDisplay(result));
|
|
845
|
+
this.lastDeliberation = result;
|
|
846
|
+
this.context.add(result);
|
|
847
|
+
this.sessionState.deliberationCount++;
|
|
848
|
+
this.sessionState.contextEntryCount = this.context.getHistory().length;
|
|
849
|
+
this.sessionState.berserkActive = false;
|
|
850
|
+
// Record session statistics — estimate tokens using CJK-aware heuristic
|
|
851
|
+
const tokens = result.rounds.reduce((sum, r) => sum + (r.opinions?.reduce((s, o) => s + estimateTokens(o.rawOutput ?? ''), 0) ?? 0), 0);
|
|
852
|
+
recordDeliberation(this.stats, result.consensus.decision, result.totalDurationMs, tokens, berserk);
|
|
853
|
+
this.stats.contextEntriesUsed = this.context.getHistory().length;
|
|
854
|
+
this.transition('READY');
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
// Safety net: ensure terminal is restored
|
|
858
|
+
if (tuiInstance) {
|
|
859
|
+
try {
|
|
860
|
+
tuiInstance.dispose();
|
|
861
|
+
}
|
|
862
|
+
catch { /* intentional: cleanup must not throw */ }
|
|
863
|
+
}
|
|
864
|
+
// P0-4: Force-restore raw mode in case TUI dispose() failed
|
|
865
|
+
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
866
|
+
try {
|
|
867
|
+
process.stdin.setRawMode(false);
|
|
868
|
+
}
|
|
869
|
+
catch { /* best-effort */ }
|
|
870
|
+
}
|
|
871
|
+
process.stdout.write(CURSOR_SHOW + ALT_SCREEN_LEAVE);
|
|
872
|
+
const msg = signal.aborted ? '思考ルーチン中断' : `思考ルーチンに異常発生: ${String(error)}`;
|
|
873
|
+
console.error(WARN_C(`\n ${msg}\n`));
|
|
874
|
+
this.sessionState.hasError = true;
|
|
875
|
+
this.sessionState.berserkActive = false;
|
|
876
|
+
// Recover to READY if not already disposing
|
|
877
|
+
if (this.state !== 'DISPOSING') {
|
|
878
|
+
const from = this.state;
|
|
879
|
+
this.state = 'READY'; // Direct assignment: error recovery from DELIBERATING or SHOWING_RESULT
|
|
880
|
+
logger.debug('REPL state transition (recovery)', { from, to: 'READY' });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
finally {
|
|
884
|
+
eventListener?.unsubscribe();
|
|
885
|
+
disposeStreaming?.();
|
|
886
|
+
process.removeListener('SIGINT', cancelHandler);
|
|
887
|
+
this.abortController = undefined;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// ── Lifecycle ─────────────────────────────────────────────
|
|
891
|
+
checkUnitsOnStartup() {
|
|
892
|
+
// Fire-and-forget: update prompt with actual unit status
|
|
893
|
+
this.magi.healthCheck().then(results => {
|
|
894
|
+
let online = 0;
|
|
895
|
+
let total = 0;
|
|
896
|
+
for (const [, result] of results) {
|
|
897
|
+
total++;
|
|
898
|
+
if (!(result instanceof Error))
|
|
899
|
+
online++;
|
|
900
|
+
}
|
|
901
|
+
this.sessionState.onlineUnits = online;
|
|
902
|
+
this.sessionState.totalUnits = total;
|
|
903
|
+
}).catch(() => {
|
|
904
|
+
// Keep default 3/3 on failure
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Graceful resource cleanup — idempotent, callable from any state.
|
|
909
|
+
* Use for programmatic shutdown without process.exit().
|
|
910
|
+
*/
|
|
911
|
+
async dispose() {
|
|
912
|
+
if (this.state === 'DISPOSING')
|
|
913
|
+
return;
|
|
914
|
+
// Abort in-flight deliberation
|
|
915
|
+
if (this.abortController) {
|
|
916
|
+
this.abortController.abort();
|
|
917
|
+
this.abortController = undefined;
|
|
918
|
+
}
|
|
919
|
+
// Direct state assignment: dispose must succeed from ANY state
|
|
920
|
+
const from = this.state;
|
|
921
|
+
this.state = 'DISPOSING';
|
|
922
|
+
logger.debug('REPL state transition (dispose)', { from, to: 'DISPOSING' });
|
|
923
|
+
// Abort background jobs and flush history
|
|
924
|
+
this.jobRegistry.dispose();
|
|
925
|
+
await this.history.flush();
|
|
926
|
+
this.history.dispose();
|
|
927
|
+
this.ghostEngine.dispose();
|
|
928
|
+
// Session summary (only if deliberations were run)
|
|
929
|
+
const summary = formatSessionSummary(this.stats);
|
|
930
|
+
if (summary) {
|
|
931
|
+
console.log('');
|
|
932
|
+
console.log(summary);
|
|
933
|
+
}
|
|
934
|
+
// Terminal state restoration
|
|
935
|
+
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
936
|
+
try {
|
|
937
|
+
process.stdin.setRawMode(false);
|
|
938
|
+
}
|
|
939
|
+
catch { /* best-effort */ }
|
|
940
|
+
}
|
|
941
|
+
process.stdout.write(CURSOR_SHOW + ALT_SCREEN_LEAVE);
|
|
942
|
+
console.log('');
|
|
943
|
+
console.log(DIM(' MAGI SYSTEM — session terminated — 全回路 正常切断'));
|
|
944
|
+
console.log('');
|
|
945
|
+
}
|
|
946
|
+
// ── Export ────────────────────────────────────────────────
|
|
947
|
+
async exportDeliberation(args) {
|
|
948
|
+
const flagJson = args.includes('--json');
|
|
949
|
+
const cleanArgs = args.replace('--json', '').trim();
|
|
950
|
+
// Generate filename — infer format from extension if user specified a filename
|
|
951
|
+
const now = new Date();
|
|
952
|
+
const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
953
|
+
const isJson = cleanArgs
|
|
954
|
+
? flagJson || cleanArgs.endsWith('.json')
|
|
955
|
+
: flagJson;
|
|
956
|
+
const ext = isJson ? 'json' : 'md';
|
|
957
|
+
const filename = cleanArgs || `magi-export-${ts}.${ext}`;
|
|
958
|
+
// Security: path traversal defense BEFORE deliberation check
|
|
959
|
+
if (filename.includes('\0')) {
|
|
960
|
+
console.log(WARN_C(' Error: invalid file path.'));
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const cwd = process.cwd();
|
|
964
|
+
const filePath = resolve(cwd, filename);
|
|
965
|
+
const rel = relative(cwd, filePath);
|
|
966
|
+
if (rel.startsWith('..') || resolve(rel) === rel) {
|
|
967
|
+
console.log(WARN_C(' Error: path traversal denied.'));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (!this.lastDeliberation) {
|
|
971
|
+
console.log(WARN_C(' No deliberation to export.'));
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const content = isJson
|
|
975
|
+
? formatDeliberationAsJson(this.lastDeliberation)
|
|
976
|
+
: formatDeliberationAsMarkdown(this.lastDeliberation);
|
|
977
|
+
try {
|
|
978
|
+
// Security: reject symlinks and warn on existing files
|
|
979
|
+
try {
|
|
980
|
+
const stat = await lstat(filePath);
|
|
981
|
+
if (stat.isSymbolicLink()) {
|
|
982
|
+
console.log(WARN_C(' Error: symlink target rejected.'));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
// File exists — overwrite with warning
|
|
986
|
+
console.log(DIM(` (overwriting existing ${rel})`));
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
// File does not exist — OK to create
|
|
990
|
+
}
|
|
991
|
+
await safeWriteFile(filePath, content);
|
|
992
|
+
console.log(OK_C(` Exported to ${rel}`));
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
console.error(WARN_C(` Export failed: ${String(error)}`));
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
clearRecentLines(lineCount) {
|
|
999
|
+
let sequence = '';
|
|
1000
|
+
for (let i = 0; i < lineCount; i++) {
|
|
1001
|
+
sequence += `\r${ERASE_LINE}`;
|
|
1002
|
+
if (i < lineCount - 1) {
|
|
1003
|
+
sequence += moveUp(1);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return sequence;
|
|
1007
|
+
}
|
|
1008
|
+
}
|