@bastani/atomic 0.8.1-1 → 0.8.2-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/CHANGELOG.md +13 -0
- package/dist/builtin/intercom/config.ts +3 -4
- package/dist/builtin/intercom/index.ts +6 -6
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/agent-dir.ts +11 -2
- package/dist/builtin/mcp/cli.js +12 -6
- package/dist/builtin/mcp/config.ts +31 -22
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/agents/agents.ts +63 -23
- package/dist/builtin/subagents/src/agents/skills.ts +21 -21
- package/dist/builtin/subagents/src/extension/index.ts +9 -8
- package/dist/builtin/subagents/src/runs/shared/run-history.ts +13 -10
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +3 -3
- package/dist/builtin/subagents/src/shared/artifacts.ts +18 -17
- package/dist/builtin/subagents/src/shared/types.ts +4 -4
- package/dist/builtin/web-access/config-paths.ts +11 -0
- package/dist/builtin/web-access/exa.ts +3 -2
- package/dist/builtin/web-access/gemini-api.ts +2 -1
- package/dist/builtin/web-access/gemini-search.ts +2 -1
- package/dist/builtin/web-access/gemini-web-config.ts +2 -1
- package/dist/builtin/web-access/github-extract.ts +2 -1
- package/dist/builtin/web-access/index.ts +11 -8
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/web-access/perplexity.ts +2 -1
- package/dist/builtin/web-access/video-extract.ts +2 -1
- package/dist/builtin/web-access/youtube-extract.ts +2 -1
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +4 -0
- package/dist/builtin/workflows/builtin/open-claude-design.ts +39 -22
- package/dist/builtin/workflows/builtin/ralph.ts +7 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/workflow/SKILL.md +28 -20
- package/dist/builtin/workflows/skills/workflow/references/design-checklist.md +8 -4
- package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +52 -23
- package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +41 -12
- package/dist/builtin/workflows/src/extension/config-loader.ts +13 -14
- package/dist/builtin/workflows/src/extension/discovery.ts +4 -6
- package/dist/builtin/workflows/src/extension/index.ts +675 -524
- package/dist/builtin/workflows/src/extension/runtime.ts +40 -16
- package/dist/builtin/workflows/src/extension/wiring.ts +3 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +43 -33
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +34 -10
- package/dist/builtin/workflows/src/shared/types.ts +1 -5
- package/dist/builtin/workflows/src/tui/graph-view.ts +245 -75
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +23 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +259 -149
- package/dist/builtin/workflows/src/tui/status-helpers.ts +3 -3
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +99 -10
- package/dist/builtin/workflows/src/tui/switcher.ts +4 -5
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +29 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -4
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +3 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +31 -8
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +9 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +11 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts +3 -2
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +25 -8
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts +3 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +97 -58
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +37 -36
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +5 -4
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +2 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +29 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +2 -2
- package/dist/core/telemetry.js.map +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +2 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +8 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/todos.d.ts.map +1 -1
- package/dist/core/tools/todos.js +3 -3
- package/dist/core/tools/todos.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -6
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/atomic-banner.d.ts +4 -0
- package/dist/modes/interactive/components/atomic-banner.d.ts.map +1 -0
- package/dist/modes/interactive/components/atomic-banner.js +34 -0
- package/dist/modes/interactive/components/atomic-banner.js.map +1 -0
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +99 -0
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-message-renderer.js +450 -0
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts +69 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-transcript.js +183 -0
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +16 -4
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +110 -137
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +2 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +2 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +192 -137
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/catppuccin-mocha.json +5 -5
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +11 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.2-0] - 2026-05-16
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Reduced the Atomic startup banner to a compact three-line mark.
|
|
10
|
+
|
|
11
|
+
## [0.8.1] - 2026-05-15
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed the Atomic changelog viewer to show only the current release notes instead of including older sections.
|
|
16
|
+
- Fixed the published `@bastani/atomic` package manifest so Bun can install it outside the monorepo without resolving private workspace-only bundled packages.
|
|
17
|
+
|
|
5
18
|
## [0.8.1-1] - 2026-05-15
|
|
6
19
|
|
|
7
20
|
### Fixed
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { CONFIG_DIR_NAME } from "@bastani/atomic";
|
|
2
|
+
import { getAgentConfigPaths } from "@bastani/atomic";
|
|
5
3
|
|
|
6
4
|
export interface IntercomConfig {
|
|
7
5
|
/** Broker command used to spawn the broker process (e.g. "npx" or "bun") */
|
|
@@ -23,7 +21,8 @@ export interface IntercomConfig {
|
|
|
23
21
|
replyHint: boolean;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
const
|
|
24
|
+
const CONFIG_PATHS = getAgentConfigPaths("intercom", "config.json");
|
|
25
|
+
const CONFIG_PATH = CONFIG_PATHS.find((path) => existsSync(path)) ?? CONFIG_PATHS[0]!;
|
|
27
26
|
|
|
28
27
|
const defaults: IntercomConfig = {
|
|
29
28
|
brokerCommand: "npx",
|
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
|
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { Text } from "@mariozechner/pi-tui";
|
|
5
|
-
import { APP_NAME } from "@bastani/atomic";
|
|
5
|
+
import { APP_NAME, getEnvValue } from "@bastani/atomic";
|
|
6
6
|
import { IntercomClient } from "./broker/client.ts";
|
|
7
7
|
import { spawnBrokerIfNeeded } from "./broker/spawn.ts";
|
|
8
8
|
import { SessionListOverlay } from "./ui/session-list.ts";
|
|
@@ -79,14 +79,14 @@ function formatAttachments(attachments: Attachment[]): string {
|
|
|
79
79
|
return text;
|
|
80
80
|
}
|
|
81
81
|
function readChildOrchestratorMetadata(): ChildOrchestratorMetadata | null {
|
|
82
|
-
const orchestratorTarget =
|
|
83
|
-
const runId =
|
|
84
|
-
const agent =
|
|
85
|
-
const index =
|
|
82
|
+
const orchestratorTarget = getEnvValue(SUBAGENT_ORCHESTRATOR_TARGET_ENV)?.trim();
|
|
83
|
+
const runId = getEnvValue(SUBAGENT_RUN_ID_ENV)?.trim();
|
|
84
|
+
const agent = getEnvValue(SUBAGENT_CHILD_AGENT_ENV)?.trim();
|
|
85
|
+
const index = getEnvValue(SUBAGENT_CHILD_INDEX_ENV)?.trim();
|
|
86
86
|
if (!orchestratorTarget || !runId || !agent || !index) {
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
|
-
const sessionName =
|
|
89
|
+
const sessionName = getEnvValue(SUBAGENT_INTERCOM_SESSION_NAME_ENV)?.trim();
|
|
90
90
|
return {
|
|
91
91
|
orchestratorTarget,
|
|
92
92
|
runId,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { APP_NAME, CONFIG_DIR_NAME } from "@bastani/atomic";
|
|
3
|
+
import { APP_NAME, CONFIG_DIR_NAME, getAgentDirs as getAtomicAgentDirs, getEnvValue } from "@bastani/atomic";
|
|
4
4
|
|
|
5
5
|
export function getAgentDir(): string {
|
|
6
|
-
const configured =
|
|
6
|
+
const configured = getEnvValue(`${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`)?.trim();
|
|
7
7
|
if (!configured) {
|
|
8
8
|
return join(homedir(), CONFIG_DIR_NAME, "agent");
|
|
9
9
|
}
|
|
@@ -16,6 +16,15 @@ export function getAgentDir(): string {
|
|
|
16
16
|
return resolve(configured);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export function getAgentDirs(): string[] {
|
|
20
|
+
const configured = getEnvValue(`${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`)?.trim();
|
|
21
|
+
return configured ? [getAgentDir()] : getAtomicAgentDirs();
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export function getAgentPath(...segments: string[]): string {
|
|
20
25
|
return join(getAgentDir(), ...segments);
|
|
21
26
|
}
|
|
27
|
+
|
|
28
|
+
export function getAgentPaths(...segments: string[]): string[] {
|
|
29
|
+
return getAgentDirs().map((dir) => join(dir, ...segments));
|
|
30
|
+
}
|
package/dist/builtin/mcp/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { APP_NAME, CONFIG_DIR_NAME } from "@bastani/atomic";
|
|
7
|
+
import { APP_NAME, CONFIG_DIR_NAME, getAgentDirs, getEnvValue, getProjectConfigPaths } from "@bastani/atomic";
|
|
8
8
|
|
|
9
9
|
const HOME = os.homedir();
|
|
10
10
|
const AGENT_DIR_ENV = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
|
@@ -15,13 +15,18 @@ function expandHome(input) {
|
|
|
15
15
|
return path.resolve(input);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const AGENT_DIR_ENV_VALUE = getEnvValue(AGENT_DIR_ENV)?.trim();
|
|
19
|
+
const AGENT_DIR = AGENT_DIR_ENV_VALUE
|
|
20
|
+
? expandHome(AGENT_DIR_ENV_VALUE)
|
|
20
21
|
: path.join(HOME, CONFIG_DIR_NAME, "agent");
|
|
21
22
|
const PI_CONFIG_PATH = path.join(AGENT_DIR, "mcp.json");
|
|
23
|
+
const PI_CONFIG_READ_PATHS = AGENT_DIR_ENV_VALUE
|
|
24
|
+
? [PI_CONFIG_PATH]
|
|
25
|
+
: getAgentDirs().map((dir) => path.join(dir, "mcp.json"));
|
|
22
26
|
const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
|
|
23
27
|
const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
|
|
24
28
|
const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), CONFIG_DIR_NAME, "mcp.json");
|
|
29
|
+
const PROJECT_PI_CONFIG_READ_PATHS = getProjectConfigPaths(process.cwd(), "mcp.json");
|
|
25
30
|
|
|
26
31
|
const IMPORT_PATHS = {
|
|
27
32
|
cursor: [path.join(HOME, ".cursor", "mcp.json")],
|
|
@@ -50,14 +55,15 @@ function readJsonFile(filePath) {
|
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
function loadPiConfig() {
|
|
53
|
-
|
|
58
|
+
const configPath = PI_CONFIG_READ_PATHS.find((candidate) => fs.existsSync(candidate));
|
|
59
|
+
if (!configPath) {
|
|
54
60
|
return { mcpServers: {} };
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
const raw = readJsonFile(
|
|
63
|
+
const raw = readJsonFile(configPath);
|
|
58
64
|
const mcpServers = raw.mcpServers ?? raw["mcp-servers"] ?? {};
|
|
59
65
|
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
60
|
-
throw new Error(`Invalid MCP config at ${
|
|
66
|
+
throw new Error(`Invalid MCP config at ${configPath}: expected \"mcpServers\" to be an object`);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
const normalized = { ...raw };
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { CONFIG_DIR_NAME } from "@bastani/atomic";
|
|
6
|
-
import { getAgentPath } from "./agent-dir.ts";
|
|
5
|
+
import { CONFIG_DIR_NAME, getProjectConfigPaths } from "@bastani/atomic";
|
|
6
|
+
import { getAgentPath, getAgentPaths } from "./agent-dir.ts";
|
|
7
7
|
import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.ts";
|
|
8
8
|
|
|
9
9
|
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
@@ -106,6 +106,10 @@ export function getProjectPiConfigPath(cwd = process.cwd()): string {
|
|
|
106
106
|
return resolve(cwd, PROJECT_PI_CONFIG_NAME);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function getProjectPiConfigPaths(cwd = process.cwd()): string[] {
|
|
110
|
+
return getProjectConfigPaths(cwd, "mcp.json").map((p) => resolve(p));
|
|
111
|
+
}
|
|
112
|
+
|
|
109
113
|
export function getConfigDiscoveryPaths(overridePath?: string, cwd = process.cwd()): ConfigDiscoveryPath[] {
|
|
110
114
|
return getConfigSources(overridePath, cwd).map((source) => ({
|
|
111
115
|
label: source.label,
|
|
@@ -195,8 +199,9 @@ export function loadMcpConfig(overridePath?: string, cwd = process.cwd()): McpCo
|
|
|
195
199
|
|
|
196
200
|
function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSourceSpec[] {
|
|
197
201
|
const userPath = getPiGlobalConfigPath(overridePath);
|
|
202
|
+
const userPaths = overridePath ? [userPath] : getAgentPaths("mcp.json");
|
|
198
203
|
const projectPath = getProjectConfigPath(cwd);
|
|
199
|
-
const
|
|
204
|
+
const projectPiPaths = getProjectPiConfigPaths(cwd);
|
|
200
205
|
const sources: ConfigSourceSpec[] = [];
|
|
201
206
|
|
|
202
207
|
if (GENERIC_GLOBAL_CONFIG_PATH !== userPath) {
|
|
@@ -212,15 +217,17 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
212
217
|
});
|
|
213
218
|
}
|
|
214
219
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
220
|
+
for (const readPath of userPaths.reverse()) {
|
|
221
|
+
sources.push({
|
|
222
|
+
id: "pi-global",
|
|
223
|
+
label: readPath === userPath ? "Pi global override" : "Pi legacy global override",
|
|
224
|
+
readPath,
|
|
225
|
+
writePath: userPath,
|
|
226
|
+
kind: "user",
|
|
227
|
+
shared: false,
|
|
228
|
+
scope: "global",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
224
231
|
|
|
225
232
|
if (projectPath !== userPath) {
|
|
226
233
|
sources.push({
|
|
@@ -234,16 +241,18 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
234
241
|
});
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
for (const projectPiPath of projectPiPaths.reverse()) {
|
|
245
|
+
if (projectPiPath !== userPath && projectPiPath !== projectPath) {
|
|
246
|
+
sources.push({
|
|
247
|
+
id: "pi-project",
|
|
248
|
+
label: projectPiPath === getProjectPiConfigPath(cwd) ? "project Pi override" : "project Pi legacy override",
|
|
249
|
+
readPath: projectPiPath,
|
|
250
|
+
writePath: getProjectPiConfigPath(cwd),
|
|
251
|
+
kind: "project",
|
|
252
|
+
shared: false,
|
|
253
|
+
scope: "project",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
247
256
|
}
|
|
248
257
|
|
|
249
258
|
return sources;
|
|
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { CONFIG_DIR_NAME } from "@bastani/atomic";
|
|
9
|
+
import { CONFIG_DIR_NAME, getAgentConfigPaths, getProjectConfigDirs } from "@bastani/atomic";
|
|
10
10
|
import type { OutputMode } from "../shared/types.ts";
|
|
11
11
|
import { KNOWN_FIELDS } from "./agent-serializer.ts";
|
|
12
12
|
import { parseChain } from "./chain-serializer.ts";
|
|
@@ -132,7 +132,15 @@ interface AgentDiscoveryResult {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function getUserChainDir(): string {
|
|
135
|
-
return path.join(os.homedir(), CONFIG_DIR_NAME, "agent", "chains");
|
|
135
|
+
return getAgentConfigPaths("chains")[0] ?? path.join(os.homedir(), CONFIG_DIR_NAME, "agent", "chains");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getUserChainDirs(): string[] {
|
|
139
|
+
return getAgentConfigPaths("chains");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getUserAgentDirs(): string[] {
|
|
143
|
+
return getAgentConfigPaths("agents");
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
|
|
@@ -207,7 +215,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
207
215
|
function findNearestProjectRoot(cwd: string): string | null {
|
|
208
216
|
let currentDir = cwd;
|
|
209
217
|
while (true) {
|
|
210
|
-
if (
|
|
218
|
+
if (getProjectConfigDirs(currentDir).some(isDirectory) || isDirectory(path.join(currentDir, ".agents"))) {
|
|
211
219
|
return currentDir;
|
|
212
220
|
}
|
|
213
221
|
|
|
@@ -218,12 +226,21 @@ function findNearestProjectRoot(cwd: string): string | null {
|
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
function getUserAgentSettingsPath(): string {
|
|
221
|
-
return path.join(os.homedir(), CONFIG_DIR_NAME, "agent", "settings.json");
|
|
229
|
+
return getAgentConfigPaths("settings.json")[0] ?? path.join(os.homedir(), CONFIG_DIR_NAME, "agent", "settings.json");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getUserAgentSettingsPaths(): string[] {
|
|
233
|
+
return getAgentConfigPaths("settings.json");
|
|
222
234
|
}
|
|
223
235
|
|
|
224
236
|
function getProjectAgentSettingsPath(cwd: string): string | null {
|
|
225
237
|
const projectRoot = findNearestProjectRoot(cwd);
|
|
226
|
-
return projectRoot ? path.join(projectRoot
|
|
238
|
+
return projectRoot ? path.join(getProjectConfigDirs(projectRoot)[0]!, "settings.json") : null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getProjectAgentSettingsPaths(cwd: string): string[] {
|
|
242
|
+
const projectRoot = findNearestProjectRoot(cwd);
|
|
243
|
+
return projectRoot ? getProjectConfigDirs(projectRoot).map((dir) => path.join(dir, "settings.json")) : [];
|
|
227
244
|
}
|
|
228
245
|
|
|
229
246
|
function readSettingsFileStrict(filePath: string): Record<string, unknown> {
|
|
@@ -413,6 +430,22 @@ function applyBuiltinOverride(
|
|
|
413
430
|
return next;
|
|
414
431
|
}
|
|
415
432
|
|
|
433
|
+
function readMergedSubagentSettings(filePaths: string[]): { settings: SubagentSettings; path: string | null } {
|
|
434
|
+
let settings = EMPTY_SUBAGENT_SETTINGS;
|
|
435
|
+
let path: string | null = null;
|
|
436
|
+
for (let i = filePaths.length - 1; i >= 0; i--) {
|
|
437
|
+
const filePath = filePaths[i]!;
|
|
438
|
+
if (!fs.existsSync(filePath)) continue;
|
|
439
|
+
const next = readSubagentSettings(filePath);
|
|
440
|
+
settings = {
|
|
441
|
+
disableBuiltins: next.disableBuiltins ?? settings.disableBuiltins,
|
|
442
|
+
overrides: { ...settings.overrides, ...next.overrides },
|
|
443
|
+
};
|
|
444
|
+
path = filePath;
|
|
445
|
+
}
|
|
446
|
+
return { settings, path };
|
|
447
|
+
}
|
|
448
|
+
|
|
416
449
|
function applyBuiltinOverrides(
|
|
417
450
|
builtinAgents: AgentConfig[],
|
|
418
451
|
userSettings: SubagentSettings,
|
|
@@ -700,10 +733,13 @@ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; pref
|
|
|
700
733
|
if (!projectRoot) return { readDirs: [], preferredDir: null };
|
|
701
734
|
|
|
702
735
|
const legacyDir = path.join(projectRoot, ".agents");
|
|
703
|
-
const preferredDir = path.join(projectRoot
|
|
736
|
+
const preferredDir = path.join(getProjectConfigDirs(projectRoot)[0]!, "agents");
|
|
704
737
|
const readDirs: string[] = [];
|
|
705
738
|
if (isDirectory(legacyDir)) readDirs.push(legacyDir);
|
|
706
|
-
|
|
739
|
+
for (const configDir of getProjectConfigDirs(projectRoot).reverse()) {
|
|
740
|
+
const agentsDir = path.join(configDir, "agents");
|
|
741
|
+
if (isDirectory(agentsDir)) readDirs.push(agentsDir);
|
|
742
|
+
}
|
|
707
743
|
|
|
708
744
|
return {
|
|
709
745
|
readDirs,
|
|
@@ -715,22 +751,24 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
|
|
|
715
751
|
const projectRoot = findNearestProjectRoot(cwd);
|
|
716
752
|
if (!projectRoot) return { readDirs: [], preferredDir: null };
|
|
717
753
|
|
|
718
|
-
const preferredDir = path.join(projectRoot
|
|
754
|
+
const preferredDir = path.join(getProjectConfigDirs(projectRoot)[0]!, "chains");
|
|
719
755
|
return {
|
|
720
|
-
readDirs:
|
|
756
|
+
readDirs: getProjectConfigDirs(projectRoot).reverse().map((configDir) => path.join(configDir, "chains")).filter(isDirectory),
|
|
721
757
|
preferredDir,
|
|
722
758
|
};
|
|
723
759
|
}
|
|
724
760
|
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
725
761
|
|
|
726
762
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
727
|
-
const userDirOld =
|
|
763
|
+
const userDirOld = getUserAgentDirs();
|
|
728
764
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
729
765
|
const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(cwd);
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
const
|
|
733
|
-
const
|
|
766
|
+
const userSettingsLoad = readMergedSubagentSettings(getUserAgentSettingsPaths());
|
|
767
|
+
const projectSettingsLoad = readMergedSubagentSettings(getProjectAgentSettingsPaths(cwd));
|
|
768
|
+
const userSettingsPath = userSettingsLoad.path ?? getUserAgentSettingsPath();
|
|
769
|
+
const projectSettingsPath = projectSettingsLoad.path ?? getProjectAgentSettingsPath(cwd);
|
|
770
|
+
const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : userSettingsLoad.settings;
|
|
771
|
+
const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : projectSettingsLoad.settings;
|
|
734
772
|
|
|
735
773
|
const builtinAgents = applyBuiltinOverrides(
|
|
736
774
|
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
@@ -740,7 +778,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
740
778
|
projectSettingsPath,
|
|
741
779
|
);
|
|
742
780
|
|
|
743
|
-
const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(
|
|
781
|
+
const userAgentsOld = scope === "project" ? [] : userDirOld.flatMap((dir) => loadAgentsFromDir(dir, "user"));
|
|
744
782
|
const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
|
|
745
783
|
const userAgents = [...userAgentsOld, ...userAgentsNew];
|
|
746
784
|
|
|
@@ -763,15 +801,17 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
763
801
|
userSettingsPath: string;
|
|
764
802
|
projectSettingsPath: string | null;
|
|
765
803
|
} {
|
|
766
|
-
const userDirOld =
|
|
804
|
+
const userDirOld = getUserAgentDirs();
|
|
767
805
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
768
806
|
const userChainDir = getUserChainDir();
|
|
769
807
|
const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
|
|
770
808
|
const { readDirs: projectChainDirs, preferredDir: projectChainDir } = resolveNearestProjectChainDirs(cwd);
|
|
771
|
-
const
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
const
|
|
809
|
+
const userSettingsLoad = readMergedSubagentSettings(getUserAgentSettingsPaths());
|
|
810
|
+
const projectSettingsLoad = readMergedSubagentSettings(getProjectAgentSettingsPaths(cwd));
|
|
811
|
+
const userSettingsPath = userSettingsLoad.path ?? getUserAgentSettingsPath();
|
|
812
|
+
const projectSettingsPath = projectSettingsLoad.path ?? getProjectAgentSettingsPath(cwd);
|
|
813
|
+
const userSettings = userSettingsLoad.settings;
|
|
814
|
+
const projectSettings = projectSettingsLoad.settings;
|
|
775
815
|
|
|
776
816
|
const builtin = applyBuiltinOverrides(
|
|
777
817
|
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
@@ -781,7 +821,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
781
821
|
projectSettingsPath,
|
|
782
822
|
);
|
|
783
823
|
const user = [
|
|
784
|
-
...loadAgentsFromDir(
|
|
824
|
+
...userDirOld.flatMap((dir) => loadAgentsFromDir(dir, "user")),
|
|
785
825
|
...loadAgentsFromDir(userDirNew, "user"),
|
|
786
826
|
];
|
|
787
827
|
const projectMap = new Map<string, AgentConfig>();
|
|
@@ -799,11 +839,11 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
799
839
|
}
|
|
800
840
|
}
|
|
801
841
|
const chains = [
|
|
802
|
-
...loadChainsFromDir(
|
|
842
|
+
...getUserChainDirs().flatMap((dir) => loadChainsFromDir(dir, "user")),
|
|
803
843
|
...Array.from(chainMap.values()),
|
|
804
844
|
];
|
|
805
845
|
|
|
806
|
-
const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld
|
|
846
|
+
const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld[0]!;
|
|
807
847
|
|
|
808
848
|
return { builtin, user, project, chains, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
809
849
|
}
|
|
@@ -6,7 +6,7 @@ import { execSync } from "node:child_process";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import {
|
|
9
|
+
import { getAgentConfigPaths, getAgentDirs, getProjectConfigDirs } from "@bastani/atomic";
|
|
10
10
|
|
|
11
11
|
export type SkillSource =
|
|
12
12
|
| "project"
|
|
@@ -50,7 +50,6 @@ const MAX_CACHE_SIZE = 50;
|
|
|
50
50
|
let loadSkillsCache: { cwd: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;
|
|
51
51
|
const LOAD_SKILLS_CACHE_TTL_MS = 5000;
|
|
52
52
|
|
|
53
|
-
const AGENT_DIR = path.join(os.homedir(), CONFIG_DIR, "agent");
|
|
54
53
|
const SUBAGENT_ORCHESTRATION_SKILL = "subagent";
|
|
55
54
|
|
|
56
55
|
const SOURCE_PRIORITY: Record<SkillSource, number> = {
|
|
@@ -135,8 +134,8 @@ function getGlobalNpmRoot(): string | null {
|
|
|
135
134
|
|
|
136
135
|
function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
|
|
137
136
|
const dirs: SkillSearchPath[] = [
|
|
138
|
-
{ path: path.join(
|
|
139
|
-
|
|
137
|
+
...getProjectConfigDirs(cwd).map((configDir) => ({ path: path.join(configDir, "npm", "node_modules"), source: "project-package" as const })),
|
|
138
|
+
...getAgentConfigPaths("npm", "node_modules").map((dir) => ({ path: dir, source: "user-package" as const })),
|
|
140
139
|
];
|
|
141
140
|
|
|
142
141
|
const globalRoot = getGlobalNpmRoot();
|
|
@@ -187,8 +186,8 @@ function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
|
|
|
187
186
|
function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
|
|
188
187
|
const results: SkillSearchPath[] = [];
|
|
189
188
|
const settingsFiles = [
|
|
190
|
-
{ file: path.join(
|
|
191
|
-
|
|
189
|
+
...getProjectConfigDirs(cwd).map((configDir) => ({ file: path.join(configDir, "settings.json"), base: configDir, source: "project-settings" as const })),
|
|
190
|
+
...getAgentConfigPaths("settings.json").map((file) => ({ file, base: path.dirname(file), source: "user-settings" as const })),
|
|
192
191
|
];
|
|
193
192
|
|
|
194
193
|
for (const { file, base, source } of settingsFiles) {
|
|
@@ -287,8 +286,8 @@ function resolveSettingsPackageRoot(source: string, baseDir: string): string | u
|
|
|
287
286
|
|
|
288
287
|
function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
|
|
289
288
|
const settingsFiles = [
|
|
290
|
-
{ file: path.join(
|
|
291
|
-
|
|
289
|
+
...getProjectConfigDirs(cwd).map((configDir) => ({ file: path.join(configDir, "settings.json"), base: configDir, source: "project-package" as const })),
|
|
290
|
+
...getAgentConfigPaths("settings.json").map((file) => ({ file, base: path.dirname(file), source: "user-package" as const })),
|
|
292
291
|
];
|
|
293
292
|
const results: SkillSearchPath[] = [];
|
|
294
293
|
|
|
@@ -317,9 +316,9 @@ function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
|
|
|
317
316
|
|
|
318
317
|
function buildSkillPaths(cwd: string): SkillSearchPath[] {
|
|
319
318
|
const skillPaths: SkillSearchPath[] = [
|
|
320
|
-
{ path: path.join(
|
|
319
|
+
...getProjectConfigDirs(cwd).map((configDir) => ({ path: path.join(configDir, "skills"), source: "project" as const })),
|
|
321
320
|
{ path: path.join(cwd, ".agents", "skills"), source: "project" },
|
|
322
|
-
{ path:
|
|
321
|
+
...getAgentConfigPaths("skills").map((dir) => ({ path: dir, source: "user" as const })),
|
|
323
322
|
{ path: path.join(os.homedir(), ".agents", "skills"), source: "user" },
|
|
324
323
|
...collectInstalledPackageSkillPaths(cwd),
|
|
325
324
|
...collectSettingsPackageSkillPaths(cwd),
|
|
@@ -340,21 +339,22 @@ function buildSkillPaths(cwd: string): SkillSearchPath[] {
|
|
|
340
339
|
function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSource): SkillSource {
|
|
341
340
|
if (sourceHint) return sourceHint;
|
|
342
341
|
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
const
|
|
342
|
+
const projectConfigRoots = getProjectConfigDirs(cwd).map((dir) => path.resolve(dir));
|
|
343
|
+
const projectSkillsRoots = projectConfigRoots.map((dir) => path.join(dir, "skills"));
|
|
344
|
+
const projectPackagesRoots = projectConfigRoots.map((dir) => path.join(dir, "npm", "node_modules"));
|
|
346
345
|
const projectAgentsRoot = path.resolve(cwd, ".agents");
|
|
347
|
-
const
|
|
348
|
-
const
|
|
346
|
+
const userAgentRoots = getAgentDirs().map((dir) => path.resolve(dir));
|
|
347
|
+
const userSkillsRoots = userAgentRoots.map((dir) => path.join(dir, "skills"));
|
|
348
|
+
const userPackagesRoots = userAgentRoots.map((dir) => path.join(dir, "npm", "node_modules"));
|
|
349
349
|
const userAgentsRoot = path.resolve(os.homedir(), ".agents");
|
|
350
350
|
|
|
351
|
-
if (isWithinPath(filePath,
|
|
352
|
-
if (isWithinPath(filePath,
|
|
353
|
-
if (isWithinPath(filePath,
|
|
351
|
+
if (projectPackagesRoots.some((root) => isWithinPath(filePath, root))) return "project-package";
|
|
352
|
+
if (projectSkillsRoots.some((root) => isWithinPath(filePath, root)) || isWithinPath(filePath, projectAgentsRoot)) return "project";
|
|
353
|
+
if (projectConfigRoots.some((root) => isWithinPath(filePath, root))) return "project-settings";
|
|
354
354
|
|
|
355
|
-
if (isWithinPath(filePath,
|
|
356
|
-
if (isWithinPath(filePath,
|
|
357
|
-
if (isWithinPath(filePath,
|
|
355
|
+
if (userPackagesRoots.some((root) => isWithinPath(filePath, root))) return "user-package";
|
|
356
|
+
if (userSkillsRoots.some((root) => isWithinPath(filePath, root)) || isWithinPath(filePath, userAgentsRoot)) return "user";
|
|
357
|
+
if (userAgentRoots.some((root) => isWithinPath(filePath, root))) return "user-settings";
|
|
358
358
|
|
|
359
359
|
const globalRoot = getGlobalNpmRoot();
|
|
360
360
|
if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
-
import {
|
|
18
|
+
import { getAgentConfigPaths, getEnvValue } from "@bastani/atomic";
|
|
19
19
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
20
20
|
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
@@ -74,13 +74,14 @@ function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function loadConfig(): ExtensionConfig {
|
|
77
|
-
const configPath
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
for (const configPath of getAgentConfigPaths("extensions", "subagent", "config.json")) {
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(configPath)) {
|
|
80
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`Failed to load subagent config from '${configPath}':`, error);
|
|
81
84
|
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error(`Failed to load subagent config from '${configPath}':`, error);
|
|
84
85
|
}
|
|
85
86
|
return {};
|
|
86
87
|
}
|
|
@@ -221,7 +222,7 @@ class SubagentControlNoticeComponent implements Component {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
224
|
-
if (
|
|
225
|
+
if (getEnvValue(SUBAGENT_CHILD_ENV) === "1") return;
|
|
225
226
|
const globalStore = globalThis as Record<string, unknown>;
|
|
226
227
|
const runtimeCleanupStoreKey = "__piSubagentRuntimeCleanup";
|
|
227
228
|
const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { getAgentConfigPaths } from "@bastani/atomic";
|
|
5
5
|
|
|
6
6
|
export interface RunEntry {
|
|
7
7
|
agent: string;
|
|
@@ -12,7 +12,8 @@ export interface RunEntry {
|
|
|
12
12
|
exit?: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const HISTORY_PATH = path.join(os.homedir(),
|
|
15
|
+
const HISTORY_PATH = getAgentConfigPaths("run-history.jsonl")[0] ?? path.join(os.homedir(), ".atomic", "agent", "run-history.jsonl");
|
|
16
|
+
const HISTORY_READ_PATHS = getAgentConfigPaths("run-history.jsonl");
|
|
16
17
|
const ROTATE_READ_THRESHOLD = 1200;
|
|
17
18
|
const ROTATE_KEEP = 1000;
|
|
18
19
|
|
|
@@ -34,15 +35,17 @@ export function recordRun(agent: string, task: string, exitCode: number, duratio
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
let lines: string[] = [];
|
|
39
|
+
for (const historyPath of HISTORY_READ_PATHS) {
|
|
40
|
+
if (!fs.existsSync(historyPath)) continue;
|
|
41
|
+
try {
|
|
42
|
+
lines.push(...fs.readFileSync(historyPath, "utf-8").split("\n"));
|
|
43
|
+
} catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
lines = lines.map((line) => line.trim()).filter((line) => line.length > 0);
|
|
48
|
+
if (lines.length === 0) return [];
|
|
46
49
|
|
|
47
50
|
if (lines.length > ROTATE_READ_THRESHOLD) {
|
|
48
51
|
lines = lines.slice(-ROTATE_KEEP);
|