@oh-my-pi/pi-coding-agent 8.12.8 → 8.12.10
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/package.json +7 -9
- package/src/config/settings-manager.ts +61 -3
- package/src/exec/bash-executor.ts +2 -2
- package/src/extensibility/plugins/installer.ts +2 -2
- package/src/extensibility/plugins/manager.ts +3 -3
- package/src/index.ts +0 -1
- package/src/ipy/gateway-coordinator.ts +12 -22
- package/src/ipy/kernel.ts +3 -3
- package/src/lsp/client.ts +27 -21
- package/src/lsp/render.ts +13 -19
- package/src/lsp/types.ts +2 -2
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/theme/theme.ts +27 -47
- package/src/tools/read.ts +174 -54
- package/src/utils/shell.ts +0 -302
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.10",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -79,22 +79,20 @@
|
|
|
79
79
|
"test": "bun test"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@oh-my-pi/omp-stats": "8.12.
|
|
83
|
-
"@oh-my-pi/pi-agent-core": "8.12.
|
|
84
|
-
"@oh-my-pi/pi-ai": "8.12.
|
|
85
|
-
"@oh-my-pi/pi-natives": "8.12.
|
|
86
|
-
"@oh-my-pi/pi-tui": "8.12.
|
|
87
|
-
"@oh-my-pi/pi-utils": "8.12.
|
|
82
|
+
"@oh-my-pi/omp-stats": "8.12.10",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "8.12.10",
|
|
84
|
+
"@oh-my-pi/pi-ai": "8.12.10",
|
|
85
|
+
"@oh-my-pi/pi-natives": "8.12.10",
|
|
86
|
+
"@oh-my-pi/pi-tui": "8.12.10",
|
|
87
|
+
"@oh-my-pi/pi-utils": "8.12.10",
|
|
88
88
|
"@openai/agents": "^0.4.4",
|
|
89
89
|
"@sinclair/typebox": "^0.34.48",
|
|
90
90
|
"ajv": "^8.17.1",
|
|
91
91
|
"chalk": "^5.6.2",
|
|
92
|
-
"cli-highlight": "^2.1.11",
|
|
93
92
|
"diff": "^8.0.3",
|
|
94
93
|
"file-type": "^21.3.0",
|
|
95
94
|
"glob": "^13.0.0",
|
|
96
95
|
"handlebars": "^4.7.8",
|
|
97
|
-
"highlight.js": "^11.11.1",
|
|
98
96
|
"marked": "^17.0.1",
|
|
99
97
|
"nanoid": "^5.1.6",
|
|
100
98
|
"node-html-parser": "^7.0.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { YAML } from "bun";
|
|
5
5
|
import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
|
|
6
6
|
import { getAgentDbPath, getAgentDir } from "../config";
|
|
@@ -461,6 +461,8 @@ export class SettingsManager {
|
|
|
461
461
|
private settings!: Settings;
|
|
462
462
|
private persist: boolean;
|
|
463
463
|
|
|
464
|
+
static #lastInstance: SettingsManager | null = null;
|
|
465
|
+
|
|
464
466
|
/**
|
|
465
467
|
* Private constructor - use static factory methods instead.
|
|
466
468
|
* @param storage - SQLite storage instance for auth/cache, or null for in-memory mode
|
|
@@ -477,6 +479,7 @@ export class SettingsManager {
|
|
|
477
479
|
initialSettings: Settings,
|
|
478
480
|
persist: boolean,
|
|
479
481
|
projectSettings: Settings,
|
|
482
|
+
private agentDir: string | null,
|
|
480
483
|
) {
|
|
481
484
|
this.storage = storage;
|
|
482
485
|
this.configPath = configPath;
|
|
@@ -517,6 +520,9 @@ export class SettingsManager {
|
|
|
517
520
|
* @returns Configured SettingsManager with merged global and user settings
|
|
518
521
|
*/
|
|
519
522
|
static async create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): Promise<SettingsManager> {
|
|
523
|
+
cwd = path.normalize(cwd);
|
|
524
|
+
agentDir = path.normalize(agentDir);
|
|
525
|
+
|
|
520
526
|
const configPath = path.join(agentDir, "config.yml");
|
|
521
527
|
const storage = await AgentStorage.open(getAgentDbPath(agentDir));
|
|
522
528
|
|
|
@@ -541,7 +547,9 @@ export class SettingsManager {
|
|
|
541
547
|
// Load project settings before construction (constructor is sync)
|
|
542
548
|
const projectSettings = await SettingsManager.loadProjectSettingsStatic(cwd);
|
|
543
549
|
|
|
544
|
-
|
|
550
|
+
const instance = new SettingsManager(storage, configPath, cwd, globalSettings, true, projectSettings, agentDir);
|
|
551
|
+
SettingsManager.#lastInstance = instance;
|
|
552
|
+
return instance;
|
|
545
553
|
}
|
|
546
554
|
|
|
547
555
|
/**
|
|
@@ -550,7 +558,7 @@ export class SettingsManager {
|
|
|
550
558
|
* @returns SettingsManager that won't persist changes to disk
|
|
551
559
|
*/
|
|
552
560
|
static inMemory(settings: Partial<Settings> = {}): SettingsManager {
|
|
553
|
-
return new SettingsManager(null, null, null, settings, false, {});
|
|
561
|
+
return new SettingsManager(null, null, null, settings, false, {}, null);
|
|
554
562
|
}
|
|
555
563
|
|
|
556
564
|
/**
|
|
@@ -1784,4 +1792,54 @@ export class SettingsManager {
|
|
|
1784
1792
|
await this.save();
|
|
1785
1793
|
}
|
|
1786
1794
|
}
|
|
1795
|
+
|
|
1796
|
+
_compareUniqueCtorKeys(cwd: string, agentDir: string): boolean {
|
|
1797
|
+
if (this.cwd !== cwd) {
|
|
1798
|
+
cwd = path.normalize(cwd);
|
|
1799
|
+
if (this.cwd !== cwd) {
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (this.agentDir !== agentDir) {
|
|
1804
|
+
agentDir = path.normalize(agentDir);
|
|
1805
|
+
if (this.agentDir !== agentDir) {
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Acquire the last created SettingsManager instance.
|
|
1814
|
+
* If no instance exists, create a new one.
|
|
1815
|
+
* @returns The SettingsManager instance
|
|
1816
|
+
*/
|
|
1817
|
+
static acquire(
|
|
1818
|
+
cwd: string = process.cwd(),
|
|
1819
|
+
agentDir: string = getAgentDir(),
|
|
1820
|
+
): SettingsManager | Promise<SettingsManager> {
|
|
1821
|
+
const prev = SettingsManager.#lastInstance;
|
|
1822
|
+
if (prev?._compareUniqueCtorKeys(cwd, agentDir)) {
|
|
1823
|
+
return prev;
|
|
1824
|
+
}
|
|
1825
|
+
return SettingsManager.create(cwd, agentDir);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Gets the shell configuration
|
|
1830
|
+
* @returns The shell configuration
|
|
1831
|
+
*/
|
|
1832
|
+
async getShellConfig() {
|
|
1833
|
+
const shell = this.getShellPath();
|
|
1834
|
+
return procmgr.getShellConfig(shell);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* Gets the shell configuration from the last created SettingsManager instance.
|
|
1839
|
+
* @returns The shell configuration
|
|
1840
|
+
*/
|
|
1841
|
+
static async getGlobalShellConfig() {
|
|
1842
|
+
const settings = await SettingsManager.acquire();
|
|
1843
|
+
return settings.getShellConfig();
|
|
1844
|
+
}
|
|
1787
1845
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Provides unified bash execution for AgentSession.executeBash() and direct calls.
|
|
5
5
|
*/
|
|
6
6
|
import { Exception, ptree } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { SettingsManager } from "../config/settings-manager";
|
|
7
8
|
import { OutputSink } from "../session/streaming-output";
|
|
8
|
-
import { getShellConfig } from "../utils/shell";
|
|
9
9
|
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
10
10
|
|
|
11
11
|
export interface BashExecutorOptions {
|
|
@@ -33,7 +33,7 @@ export interface BashResult {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
|
36
|
-
const { shell, args, env, prefix } = await
|
|
36
|
+
const { shell, args, env, prefix } = await SettingsManager.getGlobalShellConfig();
|
|
37
37
|
|
|
38
38
|
// Merge additional env vars if provided
|
|
39
39
|
const finalEnv = options?.env ? { ...env, ...options.env } : env;
|
|
@@ -45,7 +45,7 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Run npm install in plugins directory
|
|
48
|
-
const proc = Bun.spawn(["
|
|
48
|
+
const proc = Bun.spawn(["bun", "install", packageName], {
|
|
49
49
|
cwd: PLUGINS_DIR,
|
|
50
50
|
stdin: "ignore",
|
|
51
51
|
stdout: "pipe",
|
|
@@ -86,7 +86,7 @@ export async function uninstallPlugin(name: string): Promise<void> {
|
|
|
86
86
|
|
|
87
87
|
await ensurePluginsDir();
|
|
88
88
|
|
|
89
|
-
const proc = Bun.spawn(["
|
|
89
|
+
const proc = Bun.spawn(["bun", "uninstall", name], {
|
|
90
90
|
cwd: PLUGINS_DIR,
|
|
91
91
|
stdin: "ignore",
|
|
92
92
|
stdout: "pipe",
|
|
@@ -154,7 +154,7 @@ export class PluginManager {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Run npm install
|
|
157
|
-
const proc = Bun.spawn(["
|
|
157
|
+
const proc = Bun.spawn(["bun", "install", spec.packageName], {
|
|
158
158
|
cwd: getPluginsDir(),
|
|
159
159
|
stdin: "ignore",
|
|
160
160
|
stdout: "pipe",
|
|
@@ -234,7 +234,7 @@ export class PluginManager {
|
|
|
234
234
|
validatePackageName(name);
|
|
235
235
|
await this.ensurePackageJson();
|
|
236
236
|
|
|
237
|
-
const proc = Bun.spawn(["
|
|
237
|
+
const proc = Bun.spawn(["bun", "uninstall", name], {
|
|
238
238
|
cwd: getPluginsDir(),
|
|
239
239
|
stdin: "ignore",
|
|
240
240
|
stdout: "pipe",
|
|
@@ -616,7 +616,7 @@ export class PluginManager {
|
|
|
616
616
|
|
|
617
617
|
private async fixMissingPlugin(): Promise<boolean> {
|
|
618
618
|
try {
|
|
619
|
-
const proc = Bun.spawn(["
|
|
619
|
+
const proc = Bun.spawn(["bun", "install"], {
|
|
620
620
|
cwd: getPluginsDir(),
|
|
621
621
|
stdin: "ignore",
|
|
622
622
|
stdout: "pipe",
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { createServer } from "node:net";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { Subprocess } from "bun";
|
|
6
6
|
import { getAgentDir } from "../config";
|
|
7
|
-
import {
|
|
7
|
+
import { SettingsManager } from "../config/settings-manager";
|
|
8
8
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
9
9
|
import { time } from "../utils/timings";
|
|
10
10
|
|
|
@@ -304,7 +304,7 @@ async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
|
|
|
304
304
|
const lockPid = lockInfo?.pid;
|
|
305
305
|
const lockAgeMs = lockInfo?.startedAt ? Date.now() - lockInfo.startedAt : Date.now() - lockStat.mtimeMs;
|
|
306
306
|
const staleByTime = lockAgeMs > GATEWAY_LOCK_STALE_MS;
|
|
307
|
-
const staleByPid = lockPid !== undefined && !isPidRunning(lockPid);
|
|
307
|
+
const staleByPid = lockPid !== undefined && !procmgr.isPidRunning(lockPid);
|
|
308
308
|
const staleByMissingPid = lockPid === undefined && staleByTime;
|
|
309
309
|
if (staleByPid || staleByMissingPid) {
|
|
310
310
|
await fs.promises.unlink(lockPath);
|
|
@@ -365,15 +365,6 @@ async function clearGatewayInfo(): Promise<void> {
|
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
function isPidRunning(pid: number): boolean {
|
|
369
|
-
try {
|
|
370
|
-
process.kill(pid, 0);
|
|
371
|
-
return true;
|
|
372
|
-
} catch {
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
368
|
async function isGatewayHealthy(url: string): Promise<boolean> {
|
|
378
369
|
try {
|
|
379
370
|
const response = await fetch(`${url}/api/kernelspecs`, {
|
|
@@ -386,14 +377,14 @@ async function isGatewayHealthy(url: string): Promise<boolean> {
|
|
|
386
377
|
}
|
|
387
378
|
|
|
388
379
|
async function isGatewayAlive(info: GatewayInfo): Promise<boolean> {
|
|
389
|
-
if (!isPidRunning(info.pid)) return false;
|
|
380
|
+
if (!procmgr.isPidRunning(info.pid)) return false;
|
|
390
381
|
return await isGatewayHealthy(info.url);
|
|
391
382
|
}
|
|
392
383
|
|
|
393
384
|
async function startGatewayProcess(
|
|
394
385
|
cwd: string,
|
|
395
386
|
): Promise<{ url: string; pid: number; pythonPath: string; venvPath: string | null }> {
|
|
396
|
-
const { shell, env } = await
|
|
387
|
+
const { shell, env } = await SettingsManager.getGlobalShellConfig();
|
|
397
388
|
const filteredEnv = filterEnv(env);
|
|
398
389
|
const runtime = await resolvePythonRuntime(cwd, filteredEnv);
|
|
399
390
|
const snapshotPath = await getOrCreateSnapshot(shell, env).catch((err: unknown) => {
|
|
@@ -428,17 +419,16 @@ async function startGatewayProcess(
|
|
|
428
419
|
stdin: "ignore",
|
|
429
420
|
stdout: "pipe",
|
|
430
421
|
stderr: "pipe",
|
|
422
|
+
detached: true,
|
|
431
423
|
env: kernelEnv,
|
|
432
424
|
},
|
|
433
425
|
);
|
|
434
426
|
|
|
435
427
|
let exited = false;
|
|
436
428
|
gatewayProcess.exited
|
|
429
|
+
.catch(() => {})
|
|
437
430
|
.then(() => {
|
|
438
431
|
exited = true;
|
|
439
|
-
})
|
|
440
|
-
.catch(() => {
|
|
441
|
-
exited = true;
|
|
442
432
|
});
|
|
443
433
|
|
|
444
434
|
const startTime = Date.now();
|
|
@@ -459,13 +449,13 @@ async function startGatewayProcess(
|
|
|
459
449
|
await Bun.sleep(100);
|
|
460
450
|
}
|
|
461
451
|
|
|
462
|
-
await
|
|
452
|
+
await procmgr.terminate({ target: gatewayProcess, group: true });
|
|
463
453
|
throw new Error("Gateway startup timeout");
|
|
464
454
|
}
|
|
465
455
|
|
|
466
456
|
async function killGateway(pid: number, context: string): Promise<void> {
|
|
467
457
|
try {
|
|
468
|
-
await
|
|
458
|
+
await procmgr.terminate({ target: pid, group: true });
|
|
469
459
|
} catch (err) {
|
|
470
460
|
logger.warn("Failed to kill shared gateway process", {
|
|
471
461
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -495,7 +485,7 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
|
|
|
495
485
|
}
|
|
496
486
|
|
|
497
487
|
logger.debug("Cleaning up stale gateway info", { pid: existingInfo.pid });
|
|
498
|
-
if (isPidRunning(existingInfo.pid)) {
|
|
488
|
+
if (procmgr.isPidRunning(existingInfo.pid)) {
|
|
499
489
|
await killGateway(existingInfo.pid, "stale");
|
|
500
490
|
}
|
|
501
491
|
await clearGatewayInfo();
|
|
@@ -557,7 +547,7 @@ export async function getGatewayStatus(): Promise<GatewayStatus> {
|
|
|
557
547
|
venvPath: null,
|
|
558
548
|
};
|
|
559
549
|
}
|
|
560
|
-
const active = isPidRunning(info.pid);
|
|
550
|
+
const active = procmgr.isPidRunning(info.pid);
|
|
561
551
|
return {
|
|
562
552
|
active,
|
|
563
553
|
url: info.url,
|
|
@@ -573,7 +563,7 @@ export async function shutdownSharedGateway(): Promise<void> {
|
|
|
573
563
|
await withGatewayLock(async () => {
|
|
574
564
|
const info = await readGatewayInfo();
|
|
575
565
|
if (!info) return;
|
|
576
|
-
if (isPidRunning(info.pid)) {
|
|
566
|
+
if (procmgr.isPidRunning(info.pid)) {
|
|
577
567
|
await killGateway(info.pid, "shutdown");
|
|
578
568
|
}
|
|
579
569
|
await clearGatewayInfo();
|
package/src/ipy/kernel.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { logger, ptree } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { $ } from "bun";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
|
-
import {
|
|
6
|
+
import { SettingsManager } from "../config/settings-manager";
|
|
7
7
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
8
8
|
import { time } from "../utils/timings";
|
|
9
9
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
@@ -279,7 +279,7 @@ export async function checkPythonKernelAvailability(cwd: string): Promise<Python
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
try {
|
|
282
|
-
const { env } = await
|
|
282
|
+
const { env } = await SettingsManager.getGlobalShellConfig();
|
|
283
283
|
const baseEnv = filterEnv(env);
|
|
284
284
|
const runtime = await resolvePythonRuntime(cwd, baseEnv);
|
|
285
285
|
const checkScript =
|
|
@@ -611,7 +611,7 @@ export class PythonKernel {
|
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
private static async startWithLocalGateway(options: KernelStartOptions): Promise<PythonKernel> {
|
|
614
|
-
const { shell, env } = await
|
|
614
|
+
const { shell, env } = await SettingsManager.getGlobalShellConfig();
|
|
615
615
|
const filteredEnv = filterEnv(env);
|
|
616
616
|
const runtime = await resolvePythonRuntime(options.cwd, filteredEnv);
|
|
617
617
|
const snapshotPath = await getOrCreateSnapshot(shell, env).catch((err: unknown) => {
|
package/src/lsp/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { isEnoent, logger, ptree } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
3
3
|
import { applyWorkspaceEdit } from "./edits";
|
|
4
4
|
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
|
|
@@ -206,7 +206,7 @@ function concatBuffers(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
async function writeMessage(
|
|
209
|
-
sink:
|
|
209
|
+
sink: Bun.FileSink,
|
|
210
210
|
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
211
211
|
): Promise<void> {
|
|
212
212
|
const content = JSON.stringify(message);
|
|
@@ -230,7 +230,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
230
230
|
if (client.isReading) return;
|
|
231
231
|
client.isReading = true;
|
|
232
232
|
|
|
233
|
-
const reader = (client.
|
|
233
|
+
const reader = (client.proc.stdout as ReadableStream<Uint8Array>).getReader();
|
|
234
234
|
|
|
235
235
|
try {
|
|
236
236
|
while (true) {
|
|
@@ -364,7 +364,7 @@ async function sendResponse(
|
|
|
364
364
|
};
|
|
365
365
|
|
|
366
366
|
try {
|
|
367
|
-
await writeMessage(client.
|
|
367
|
+
await writeMessage(client.proc.stdin, response);
|
|
368
368
|
} catch (err) {
|
|
369
369
|
logger.error("LSP failed to respond.", { method, error: String(err) });
|
|
370
370
|
}
|
|
@@ -409,18 +409,17 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
409
409
|
? await getLspmuxCommand(baseCommand, baseArgs)
|
|
410
410
|
: { command: baseCommand, args: baseArgs };
|
|
411
411
|
|
|
412
|
-
const proc =
|
|
412
|
+
const proc = ptree.spawn([command, ...args], {
|
|
413
413
|
cwd,
|
|
414
|
+
detached: true,
|
|
414
415
|
stdin: "pipe",
|
|
415
|
-
stdout: "pipe",
|
|
416
|
-
stderr: "pipe",
|
|
417
416
|
env: env ? { ...process.env, ...env } : undefined,
|
|
418
417
|
});
|
|
419
418
|
|
|
420
419
|
const client: LspClient = {
|
|
421
420
|
name: key,
|
|
422
421
|
cwd,
|
|
423
|
-
|
|
422
|
+
proc,
|
|
424
423
|
config,
|
|
425
424
|
requestId: 0,
|
|
426
425
|
diagnostics: new Map(),
|
|
@@ -686,7 +685,7 @@ export function shutdownClient(key: string): void {
|
|
|
686
685
|
sendRequest(client, "shutdown", null).catch(() => {});
|
|
687
686
|
|
|
688
687
|
// Kill process
|
|
689
|
-
client.
|
|
688
|
+
client.proc.kill();
|
|
690
689
|
clients.delete(key);
|
|
691
690
|
}
|
|
692
691
|
|
|
@@ -773,7 +772,7 @@ export async function sendRequest(
|
|
|
773
772
|
});
|
|
774
773
|
|
|
775
774
|
// Write request
|
|
776
|
-
writeMessage(client.
|
|
775
|
+
writeMessage(client.proc.stdin, request).catch(err => {
|
|
777
776
|
if (timeout) clearTimeout(timeout);
|
|
778
777
|
client.pendingRequests.delete(id);
|
|
779
778
|
cleanup();
|
|
@@ -793,26 +792,33 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
793
792
|
};
|
|
794
793
|
|
|
795
794
|
client.lastActivity = Date.now();
|
|
796
|
-
await writeMessage(client.
|
|
795
|
+
await writeMessage(client.proc.stdin, notification);
|
|
797
796
|
}
|
|
798
797
|
|
|
799
798
|
/**
|
|
800
799
|
* Shutdown all LSP clients.
|
|
801
800
|
*/
|
|
802
801
|
export function shutdownAll(): void {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
806
|
-
pending.reject(new Error("LSP client shutdown"));
|
|
807
|
-
}
|
|
808
|
-
client.pendingRequests.clear();
|
|
802
|
+
const clientsToShutdown = Array.from(clients.values());
|
|
803
|
+
clients.clear();
|
|
809
804
|
|
|
810
|
-
|
|
811
|
-
|
|
805
|
+
const err = new Error("LSP client shutdown");
|
|
806
|
+
for (const client of clientsToShutdown) {
|
|
807
|
+
/// Reject all pending requests
|
|
808
|
+
const reqs = Array.from(client.pendingRequests.values());
|
|
809
|
+
client.pendingRequests.clear();
|
|
810
|
+
for (const pending of reqs) {
|
|
811
|
+
pending.reject(err);
|
|
812
|
+
}
|
|
812
813
|
|
|
813
|
-
|
|
814
|
+
void (async () => {
|
|
815
|
+
// Send shutdown request (best effort, don't wait)
|
|
816
|
+
const timeout = Bun.sleep(5_000);
|
|
817
|
+
const result = sendRequest(client, "shutdown", null).catch(() => {});
|
|
818
|
+
await Promise.race([result, timeout]);
|
|
819
|
+
client.proc.kill();
|
|
820
|
+
})().catch(() => {});
|
|
814
821
|
}
|
|
815
|
-
clients.clear();
|
|
816
822
|
}
|
|
817
823
|
|
|
818
824
|
/** Status of an LSP server */
|
package/src/lsp/render.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* - Collapsible/expandable views
|
|
9
9
|
*/
|
|
10
10
|
import type { RenderResultOptions } from "@oh-my-pi/pi-agent-core";
|
|
11
|
+
import { type HighlightColors, highlightCode as nativeHighlightCode, supportsLanguage } from "@oh-my-pi/pi-natives";
|
|
11
12
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
12
|
-
import { highlight, supportsLanguage } from "cli-highlight";
|
|
13
13
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
14
14
|
import {
|
|
15
15
|
formatExpandHint,
|
|
@@ -291,29 +291,23 @@ function renderHover(
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
/**
|
|
294
|
-
* Syntax highlight code using
|
|
294
|
+
* Syntax highlight code using native WASM highlighter.
|
|
295
295
|
*/
|
|
296
296
|
function highlightCode(codeText: string, language: string, theme: Theme): string[] {
|
|
297
297
|
const validLang = language && supportsLanguage(language) ? language : undefined;
|
|
298
298
|
try {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
string:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
type: (s: string) => theme.fg("syntaxType", s),
|
|
310
|
-
attr: (s: string) => theme.fg("syntaxVariable", s),
|
|
311
|
-
variable: (s: string) => theme.fg("syntaxVariable", s),
|
|
312
|
-
params: (s: string) => theme.fg("syntaxVariable", s),
|
|
313
|
-
operator: (s: string) => theme.fg("syntaxOperator", s),
|
|
314
|
-
punctuation: (s: string) => theme.fg("syntaxPunctuation", s),
|
|
299
|
+
const colors: HighlightColors = {
|
|
300
|
+
comment: theme.getFgAnsi("syntaxComment"),
|
|
301
|
+
keyword: theme.getFgAnsi("syntaxKeyword"),
|
|
302
|
+
function: theme.getFgAnsi("syntaxFunction"),
|
|
303
|
+
variable: theme.getFgAnsi("syntaxVariable"),
|
|
304
|
+
string: theme.getFgAnsi("syntaxString"),
|
|
305
|
+
number: theme.getFgAnsi("syntaxNumber"),
|
|
306
|
+
type: theme.getFgAnsi("syntaxType"),
|
|
307
|
+
operator: theme.getFgAnsi("syntaxOperator"),
|
|
308
|
+
punctuation: theme.getFgAnsi("syntaxPunctuation"),
|
|
315
309
|
};
|
|
316
|
-
return
|
|
310
|
+
return nativeHighlightCode(codeText, validLang, colors).split("\n");
|
|
317
311
|
} catch {
|
|
318
312
|
return codeText.split("\n");
|
|
319
313
|
}
|
package/src/lsp/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { ptree } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
-
import type { Subprocess } from "bun";
|
|
4
4
|
|
|
5
5
|
// =============================================================================
|
|
6
6
|
// Tool Schema
|
|
@@ -400,7 +400,7 @@ export interface LspClient {
|
|
|
400
400
|
name: string;
|
|
401
401
|
cwd: string;
|
|
402
402
|
config: ServerConfig;
|
|
403
|
-
|
|
403
|
+
proc: ptree.ChildProcess<"pipe">;
|
|
404
404
|
requestId: number;
|
|
405
405
|
diagnostics: Map<string, Diagnostic[]>;
|
|
406
406
|
diagnosticsVersion: number;
|
|
@@ -524,7 +524,7 @@ export class RpcClient {
|
|
|
524
524
|
|
|
525
525
|
// Write to stdin after registering the handler
|
|
526
526
|
const stdin = this.process!.stdin as import("bun").FileSink;
|
|
527
|
-
stdin.write(
|
|
527
|
+
stdin.write(`${JSON.stringify(fullCommand)}\n`);
|
|
528
528
|
// flush() returns number | Promise<number> - handle both cases
|
|
529
529
|
const flushResult = stdin.flush();
|
|
530
530
|
if (flushResult instanceof Promise) {
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type HighlightColors as NativeHighlightColors,
|
|
5
|
+
highlightCode as nativeHighlightCode,
|
|
6
|
+
supportsLanguage as nativeSupportsLanguage,
|
|
7
|
+
} from "@oh-my-pi/pi-natives";
|
|
3
8
|
import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
|
|
4
9
|
import { adjustHsv, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
5
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
6
11
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
7
12
|
import chalk from "chalk";
|
|
8
|
-
import { highlight, supportsLanguage } from "cli-highlight";
|
|
9
13
|
import { getCustomThemesDir } from "../../config";
|
|
10
14
|
// Embed theme JSON files at build time
|
|
11
15
|
import darkThemeJson from "./dark.json" with { type: "json" };
|
|
@@ -2029,37 +2033,25 @@ export async function getThemeExportColors(themeName?: string): Promise<{
|
|
|
2029
2033
|
// TUI Helpers
|
|
2030
2034
|
// ============================================================================
|
|
2031
2035
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
attr: (s: string) => t.fg("syntaxVariable", s),
|
|
2050
|
-
variable: (s: string) => t.fg("syntaxVariable", s),
|
|
2051
|
-
params: (s: string) => t.fg("syntaxVariable", s),
|
|
2052
|
-
operator: (s: string) => t.fg("syntaxOperator", s),
|
|
2053
|
-
punctuation: (s: string) => t.fg("syntaxPunctuation", s),
|
|
2054
|
-
};
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
function getCliHighlightTheme(t: Theme): CliHighlightTheme {
|
|
2058
|
-
if (cachedHighlightThemeFor !== t || !cachedCliHighlightTheme) {
|
|
2059
|
-
cachedHighlightThemeFor = t;
|
|
2060
|
-
cachedCliHighlightTheme = buildCliHighlightTheme(t);
|
|
2036
|
+
let cachedHighlightColorsFor: Theme | undefined;
|
|
2037
|
+
let cachedHighlightColors: NativeHighlightColors | undefined;
|
|
2038
|
+
|
|
2039
|
+
function getHighlightColors(t: Theme): NativeHighlightColors {
|
|
2040
|
+
if (cachedHighlightColorsFor !== t || !cachedHighlightColors) {
|
|
2041
|
+
cachedHighlightColorsFor = t;
|
|
2042
|
+
cachedHighlightColors = {
|
|
2043
|
+
comment: t.getFgAnsi("syntaxComment"),
|
|
2044
|
+
keyword: t.getFgAnsi("syntaxKeyword"),
|
|
2045
|
+
function: t.getFgAnsi("syntaxFunction"),
|
|
2046
|
+
variable: t.getFgAnsi("syntaxVariable"),
|
|
2047
|
+
string: t.getFgAnsi("syntaxString"),
|
|
2048
|
+
number: t.getFgAnsi("syntaxNumber"),
|
|
2049
|
+
type: t.getFgAnsi("syntaxType"),
|
|
2050
|
+
operator: t.getFgAnsi("syntaxOperator"),
|
|
2051
|
+
punctuation: t.getFgAnsi("syntaxPunctuation"),
|
|
2052
|
+
};
|
|
2061
2053
|
}
|
|
2062
|
-
return
|
|
2054
|
+
return cachedHighlightColors;
|
|
2063
2055
|
}
|
|
2064
2056
|
|
|
2065
2057
|
/**
|
|
@@ -2067,15 +2059,9 @@ function getCliHighlightTheme(t: Theme): CliHighlightTheme {
|
|
|
2067
2059
|
* Returns array of highlighted lines.
|
|
2068
2060
|
*/
|
|
2069
2061
|
export function highlightCode(code: string, lang?: string): string[] {
|
|
2070
|
-
|
|
2071
|
-
const validLang = lang && supportsLanguage(lang) ? lang : undefined;
|
|
2072
|
-
const opts = {
|
|
2073
|
-
language: validLang,
|
|
2074
|
-
ignoreIllegals: true,
|
|
2075
|
-
theme: getCliHighlightTheme(theme),
|
|
2076
|
-
};
|
|
2062
|
+
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
|
|
2077
2063
|
try {
|
|
2078
|
-
return
|
|
2064
|
+
return nativeHighlightCode(code, validLang, getHighlightColors(theme)).split("\n");
|
|
2079
2065
|
} catch {
|
|
2080
2066
|
return code.split("\n");
|
|
2081
2067
|
}
|
|
@@ -2212,15 +2198,9 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
2212
2198
|
symbols: getSymbolTheme(),
|
|
2213
2199
|
getMermaidImage,
|
|
2214
2200
|
highlightCode: (code: string, lang?: string): string[] => {
|
|
2215
|
-
|
|
2216
|
-
const validLang = lang && supportsLanguage(lang) ? lang : undefined;
|
|
2217
|
-
const opts = {
|
|
2218
|
-
language: validLang,
|
|
2219
|
-
ignoreIllegals: true,
|
|
2220
|
-
theme: getCliHighlightTheme(theme),
|
|
2221
|
-
};
|
|
2201
|
+
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
|
|
2222
2202
|
try {
|
|
2223
|
-
return
|
|
2203
|
+
return nativeHighlightCode(code, validLang, getHighlightColors(theme)).split("\n");
|
|
2224
2204
|
} catch {
|
|
2225
2205
|
return code.split("\n").map(line => theme.fg("mdCodeBlock", line));
|
|
2226
2206
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
@@ -43,77 +44,183 @@ function isRemoteMountPath(absolutePath: string): boolean {
|
|
|
43
44
|
return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* Avoids loading the entire file into memory for large files.
|
|
49
|
-
*
|
|
50
|
-
* @param filePath - Path to the file
|
|
51
|
-
* @param startLine - 0-indexed start line
|
|
52
|
-
* @param maxLinesToCollect - Maximum lines to collect (from startLine)
|
|
53
|
-
* @param maxBytes - Maximum bytes to collect
|
|
54
|
-
* @returns Collected lines, total line count, and truncation info
|
|
55
|
-
*/
|
|
47
|
+
const READ_CHUNK_SIZE = 64 * 1024;
|
|
48
|
+
|
|
56
49
|
async function streamLinesFromFile(
|
|
57
50
|
filePath: string,
|
|
58
51
|
startLine: number,
|
|
59
52
|
maxLinesToCollect: number,
|
|
60
53
|
maxBytes: number,
|
|
54
|
+
selectedLineLimit: number | null,
|
|
55
|
+
signal?: AbortSignal,
|
|
61
56
|
): Promise<{
|
|
62
57
|
lines: string[];
|
|
63
58
|
totalFileLines: number;
|
|
64
59
|
collectedBytes: number;
|
|
65
60
|
stoppedByByteLimit: boolean;
|
|
61
|
+
firstLinePreview?: { text: string; bytes: number };
|
|
62
|
+
firstLineByteLength?: number;
|
|
63
|
+
selectedBytesTotal: number;
|
|
66
64
|
}> {
|
|
67
|
-
const
|
|
68
|
-
const decoder = new TextDecoder();
|
|
69
|
-
|
|
65
|
+
const bufferChunk = Buffer.allocUnsafe(READ_CHUNK_SIZE);
|
|
70
66
|
const collectedLines: string[] = [];
|
|
71
67
|
let lineIndex = 0;
|
|
72
68
|
let collectedBytes = 0;
|
|
73
69
|
let stoppedByByteLimit = false;
|
|
74
|
-
let buffer = "";
|
|
75
70
|
let doneCollecting = false;
|
|
71
|
+
let fileHandle: fs.FileHandle | null = null;
|
|
72
|
+
let currentLineLength = 0;
|
|
73
|
+
let currentLineChunks: Buffer[] = [];
|
|
74
|
+
let sawAnyByte = false;
|
|
75
|
+
let endedWithNewline = false;
|
|
76
|
+
let firstLinePreviewBytes = 0;
|
|
77
|
+
const firstLinePreviewChunks: Buffer[] = [];
|
|
78
|
+
let firstLineByteLength: number | undefined;
|
|
79
|
+
let selectedBytesTotal = 0;
|
|
80
|
+
let selectedLinesSeen = 0;
|
|
81
|
+
let captureLine = false;
|
|
82
|
+
let discardLineChunks = false;
|
|
83
|
+
let lineCaptureLimit = 0;
|
|
84
|
+
|
|
85
|
+
const setupLineState = () => {
|
|
86
|
+
captureLine = !doneCollecting && lineIndex >= startLine;
|
|
87
|
+
discardLineChunks = !captureLine;
|
|
88
|
+
if (captureLine) {
|
|
89
|
+
const separatorBytes = collectedLines.length > 0 ? 1 : 0;
|
|
90
|
+
lineCaptureLimit = maxBytes - collectedBytes - separatorBytes;
|
|
91
|
+
if (lineCaptureLimit <= 0) {
|
|
92
|
+
discardLineChunks = true;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
lineCaptureLimit = 0;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const decodeLine = (): string => {
|
|
100
|
+
if (currentLineLength === 0) return "";
|
|
101
|
+
if (currentLineChunks.length === 1 && currentLineChunks[0]?.length === currentLineLength) {
|
|
102
|
+
return currentLineChunks[0].toString("utf-8");
|
|
103
|
+
}
|
|
104
|
+
return Buffer.concat(currentLineChunks, currentLineLength).toString("utf-8");
|
|
105
|
+
};
|
|
76
106
|
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
const maybeCapturePreview = (segment: Uint8Array) => {
|
|
108
|
+
if (doneCollecting || lineIndex < startLine || collectedLines.length !== 0) return;
|
|
109
|
+
if (firstLinePreviewBytes >= maxBytes || segment.length === 0) return;
|
|
110
|
+
const remaining = maxBytes - firstLinePreviewBytes;
|
|
111
|
+
const slice = segment.length > remaining ? segment.subarray(0, remaining) : segment;
|
|
112
|
+
if (slice.length === 0) return;
|
|
113
|
+
firstLinePreviewChunks.push(Buffer.from(slice));
|
|
114
|
+
firstLinePreviewBytes += slice.length;
|
|
115
|
+
};
|
|
79
116
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
117
|
+
const appendSegment = (segment: Uint8Array) => {
|
|
118
|
+
currentLineLength += segment.length;
|
|
119
|
+
maybeCapturePreview(segment);
|
|
120
|
+
if (!captureLine || discardLineChunks || segment.length === 0) return;
|
|
121
|
+
if (currentLineLength <= lineCaptureLimit) {
|
|
122
|
+
currentLineChunks.push(Buffer.from(segment));
|
|
123
|
+
} else {
|
|
124
|
+
discardLineChunks = true;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
83
127
|
|
|
84
|
-
|
|
85
|
-
|
|
128
|
+
const finalizeLine = () => {
|
|
129
|
+
if (lineIndex >= startLine && (selectedLineLimit === null || selectedLinesSeen < selectedLineLimit)) {
|
|
130
|
+
selectedBytesTotal += currentLineLength + (selectedLinesSeen > 0 ? 1 : 0);
|
|
131
|
+
selectedLinesSeen++;
|
|
132
|
+
}
|
|
86
133
|
|
|
87
|
-
|
|
134
|
+
if (!doneCollecting && lineIndex >= startLine) {
|
|
135
|
+
const separatorBytes = collectedLines.length > 0 ? 1 : 0;
|
|
136
|
+
if (collectedLines.length >= maxLinesToCollect) {
|
|
137
|
+
doneCollecting = true;
|
|
138
|
+
} else if (collectedLines.length === 0 && currentLineLength > maxBytes) {
|
|
139
|
+
stoppedByByteLimit = true;
|
|
140
|
+
doneCollecting = true;
|
|
141
|
+
if (firstLineByteLength === undefined) {
|
|
142
|
+
firstLineByteLength = currentLineLength;
|
|
143
|
+
}
|
|
144
|
+
} else if (collectedLines.length > 0 && collectedBytes + separatorBytes + currentLineLength > maxBytes) {
|
|
145
|
+
stoppedByByteLimit = true;
|
|
146
|
+
doneCollecting = true;
|
|
147
|
+
} else {
|
|
148
|
+
const lineText = decodeLine();
|
|
149
|
+
collectedLines.push(lineText);
|
|
150
|
+
collectedBytes += separatorBytes + currentLineLength;
|
|
151
|
+
if (firstLineByteLength === undefined) {
|
|
152
|
+
firstLineByteLength = currentLineLength;
|
|
153
|
+
}
|
|
154
|
+
if (collectedBytes > maxBytes) {
|
|
88
155
|
stoppedByByteLimit = true;
|
|
89
156
|
doneCollecting = true;
|
|
90
|
-
} else if (collectedLines.length
|
|
91
|
-
collectedLines.push(line);
|
|
92
|
-
collectedBytes += lineBytes;
|
|
93
|
-
if (collectedLines.length >= maxLinesToCollect) {
|
|
94
|
-
doneCollecting = true;
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
157
|
+
} else if (collectedLines.length >= maxLinesToCollect) {
|
|
97
158
|
doneCollecting = true;
|
|
98
159
|
}
|
|
99
160
|
}
|
|
161
|
+
} else if (lineIndex >= startLine && firstLineByteLength === undefined) {
|
|
162
|
+
firstLineByteLength = currentLineLength;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lineIndex++;
|
|
166
|
+
currentLineLength = 0;
|
|
167
|
+
currentLineChunks = [];
|
|
168
|
+
setupLineState();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
setupLineState();
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
fileHandle = await fs.open(filePath, "r");
|
|
175
|
+
|
|
176
|
+
while (true) {
|
|
177
|
+
throwIfAborted(signal);
|
|
178
|
+
const { bytesRead } = await fileHandle.read(bufferChunk, 0, bufferChunk.length, null);
|
|
179
|
+
if (bytesRead === 0) break;
|
|
180
|
+
|
|
181
|
+
sawAnyByte = true;
|
|
182
|
+
const chunk = bufferChunk.subarray(0, bytesRead);
|
|
183
|
+
endedWithNewline = chunk[bytesRead - 1] === 0x0a;
|
|
184
|
+
|
|
185
|
+
let start = 0;
|
|
186
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
187
|
+
if (chunk[i] === 0x0a) {
|
|
188
|
+
const segment = chunk.subarray(start, i);
|
|
189
|
+
if (segment.length > 0) {
|
|
190
|
+
appendSegment(segment);
|
|
191
|
+
}
|
|
192
|
+
finalizeLine();
|
|
193
|
+
start = i + 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
100
196
|
|
|
101
|
-
|
|
197
|
+
if (start < chunk.length) {
|
|
198
|
+
appendSegment(chunk.subarray(start));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
if (fileHandle) {
|
|
203
|
+
await fileHandle.close();
|
|
102
204
|
}
|
|
103
205
|
}
|
|
104
206
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
207
|
+
if (endedWithNewline || currentLineLength > 0 || !sawAnyByte) {
|
|
208
|
+
finalizeLine();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let firstLinePreview: { text: string; bytes: number } | undefined;
|
|
212
|
+
if (firstLinePreviewBytes > 0) {
|
|
213
|
+
const buf = Buffer.concat(firstLinePreviewChunks, firstLinePreviewBytes);
|
|
214
|
+
let end = Math.min(buf.length, maxBytes);
|
|
215
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80) {
|
|
216
|
+
end--;
|
|
217
|
+
}
|
|
218
|
+
if (end > 0) {
|
|
219
|
+
const text = buf.slice(0, end).toString("utf-8");
|
|
220
|
+
firstLinePreview = { text, bytes: Buffer.byteLength(text, "utf-8") };
|
|
221
|
+
} else {
|
|
222
|
+
firstLinePreview = { text: "", bytes: 0 };
|
|
115
223
|
}
|
|
116
|
-
lineIndex++;
|
|
117
224
|
}
|
|
118
225
|
|
|
119
226
|
return {
|
|
@@ -121,6 +228,9 @@ async function streamLinesFromFile(
|
|
|
121
228
|
totalFileLines: lineIndex,
|
|
122
229
|
collectedBytes,
|
|
123
230
|
stoppedByByteLimit,
|
|
231
|
+
firstLinePreview,
|
|
232
|
+
firstLineByteLength,
|
|
233
|
+
selectedBytesTotal,
|
|
124
234
|
};
|
|
125
235
|
}
|
|
126
236
|
|
|
@@ -599,13 +709,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
599
709
|
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
600
710
|
const startLineDisplay = startLine + 1; // For display (1-indexed)
|
|
601
711
|
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
712
|
+
const maxLinesToCollect = limit !== undefined ? Math.min(limit, DEFAULT_MAX_LINES) : DEFAULT_MAX_LINES;
|
|
713
|
+
const selectedLineLimit = limit ?? null;
|
|
714
|
+
const streamResult = await streamLinesFromFile(
|
|
715
|
+
absolutePath,
|
|
716
|
+
startLine,
|
|
717
|
+
maxLinesToCollect,
|
|
718
|
+
DEFAULT_MAX_BYTES,
|
|
719
|
+
selectedLineLimit,
|
|
720
|
+
signal,
|
|
721
|
+
);
|
|
607
722
|
|
|
608
|
-
const {
|
|
723
|
+
const {
|
|
724
|
+
lines: collectedLines,
|
|
725
|
+
totalFileLines,
|
|
726
|
+
collectedBytes,
|
|
727
|
+
stoppedByByteLimit,
|
|
728
|
+
firstLinePreview,
|
|
729
|
+
firstLineByteLength,
|
|
730
|
+
} = streamResult;
|
|
609
731
|
|
|
610
732
|
// Check if offset is out of bounds - return graceful message instead of throwing
|
|
611
733
|
if (startLine >= totalFileLines) {
|
|
@@ -618,14 +740,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
618
740
|
.done();
|
|
619
741
|
}
|
|
620
742
|
|
|
621
|
-
// Build the selected content from collected lines
|
|
622
743
|
const selectedContent = collectedLines.join("\n");
|
|
623
744
|
const userLimitedLines = limit !== undefined ? collectedLines.length : undefined;
|
|
624
745
|
|
|
625
|
-
// Build truncation result from streaming data
|
|
626
746
|
const totalSelectedLines = totalFileLines - startLine;
|
|
627
|
-
const totalSelectedBytes = collectedBytes;
|
|
747
|
+
const totalSelectedBytes = collectedBytes;
|
|
628
748
|
const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
|
|
749
|
+
const firstLineExceedsLimit = firstLineByteLength !== undefined && firstLineByteLength > DEFAULT_MAX_BYTES;
|
|
629
750
|
|
|
630
751
|
const truncation: TruncationResult = {
|
|
631
752
|
content: selectedContent,
|
|
@@ -636,7 +757,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
636
757
|
outputLines: collectedLines.length,
|
|
637
758
|
outputBytes: collectedBytes,
|
|
638
759
|
lastLinePartial: false,
|
|
639
|
-
firstLineExceedsLimit
|
|
760
|
+
firstLineExceedsLimit,
|
|
640
761
|
maxLines: DEFAULT_MAX_LINES,
|
|
641
762
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
642
763
|
};
|
|
@@ -658,9 +779,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
658
779
|
let outputText: string;
|
|
659
780
|
|
|
660
781
|
if (truncation.firstLineExceedsLimit) {
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
|
|
782
|
+
const firstLineBytes = firstLineByteLength ?? 0;
|
|
783
|
+
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
664
784
|
|
|
665
785
|
outputText = shouldAddLineNumbers ? prependLineNumbers(snippet.text, startLineDisplay) : snippet.text;
|
|
666
786
|
if (snippet.text.length === 0) {
|
package/src/utils/shell.ts
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import { $ } from "bun";
|
|
3
|
-
import { SettingsManager } from "../config/settings-manager";
|
|
4
|
-
|
|
5
|
-
export interface ShellConfig {
|
|
6
|
-
shell: string;
|
|
7
|
-
args: string[];
|
|
8
|
-
env: Record<string, string | undefined>;
|
|
9
|
-
prefix: string | undefined;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let cachedShellConfig: ShellConfig | null = null;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Check if a shell binary is executable.
|
|
16
|
-
*/
|
|
17
|
-
async function isExecutable(path: string): Promise<boolean> {
|
|
18
|
-
try {
|
|
19
|
-
await fs.promises.access(path, fs.constants.X_OK);
|
|
20
|
-
return true;
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Build the spawn environment (cached).
|
|
28
|
-
*/
|
|
29
|
-
function buildSpawnEnv(shell: string): Record<string, string | undefined> {
|
|
30
|
-
const noCI = process.env.OMP_BASH_NO_CI || process.env.CLAUDE_BASH_NO_CI;
|
|
31
|
-
return {
|
|
32
|
-
...process.env,
|
|
33
|
-
SHELL: shell,
|
|
34
|
-
GIT_EDITOR: "true",
|
|
35
|
-
GPG_TTY: "not a tty",
|
|
36
|
-
OMPCODE: "1",
|
|
37
|
-
CLAUDECODE: "1",
|
|
38
|
-
...(noCI ? {} : { CI: "true" }),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get shell args, optionally including login shell flag.
|
|
44
|
-
* Supports OMP_BASH_NO_LOGIN and CLAUDE_BASH_NO_LOGIN to skip -l.
|
|
45
|
-
*/
|
|
46
|
-
function getShellArgs(): string[] {
|
|
47
|
-
const noLogin = process.env.OMP_BASH_NO_LOGIN || process.env.CLAUDE_BASH_NO_LOGIN;
|
|
48
|
-
return noLogin ? ["-c"] : ["-l", "-c"];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get shell prefix for wrapping commands (profilers, strace, etc.).
|
|
53
|
-
*/
|
|
54
|
-
function getShellPrefix(): string | undefined {
|
|
55
|
-
return process.env.OMP_SHELL_PREFIX || process.env.CLAUDE_CODE_SHELL_PREFIX;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Find bash executable on PATH (Windows)
|
|
60
|
-
*/
|
|
61
|
-
function findBashOnPath(): string | null {
|
|
62
|
-
try {
|
|
63
|
-
return Bun.which("bash.exe");
|
|
64
|
-
} catch {
|
|
65
|
-
// Ignore errors
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Build full shell config from a shell path.
|
|
72
|
-
*/
|
|
73
|
-
function buildConfig(shell: string): ShellConfig {
|
|
74
|
-
return {
|
|
75
|
-
shell,
|
|
76
|
-
args: getShellArgs(),
|
|
77
|
-
env: buildSpawnEnv(shell),
|
|
78
|
-
prefix: getShellPrefix(),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get shell configuration based on platform.
|
|
84
|
-
* Resolution order:
|
|
85
|
-
* 1. User-specified shellPath in settings.json
|
|
86
|
-
* 2. On Windows: Git Bash in known locations, then bash on PATH
|
|
87
|
-
* 3. On Unix: $SHELL if bash/zsh, then fallback paths
|
|
88
|
-
* 4. Fallback: sh
|
|
89
|
-
*/
|
|
90
|
-
export async function getShellConfig(): Promise<ShellConfig> {
|
|
91
|
-
if (cachedShellConfig) {
|
|
92
|
-
return cachedShellConfig;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const settings = await SettingsManager.create();
|
|
96
|
-
const customShellPath = settings.getShellPath();
|
|
97
|
-
|
|
98
|
-
// 1. Check user-specified shell path
|
|
99
|
-
if (customShellPath) {
|
|
100
|
-
if (await Bun.file(customShellPath).exists()) {
|
|
101
|
-
cachedShellConfig = buildConfig(customShellPath);
|
|
102
|
-
return cachedShellConfig;
|
|
103
|
-
}
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Custom shell path not found: ${customShellPath}\nPlease update shellPath in ~/.omp/agent/settings.json`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (process.platform === "win32") {
|
|
110
|
-
// 2. Try Git Bash in known locations
|
|
111
|
-
const paths: string[] = [];
|
|
112
|
-
const programFiles = process.env.ProgramFiles;
|
|
113
|
-
if (programFiles) {
|
|
114
|
-
paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
|
|
115
|
-
}
|
|
116
|
-
const programFilesX86 = process.env["ProgramFiles(x86)"];
|
|
117
|
-
if (programFilesX86) {
|
|
118
|
-
paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
for (const path of paths) {
|
|
122
|
-
if (await Bun.file(path).exists()) {
|
|
123
|
-
cachedShellConfig = buildConfig(path);
|
|
124
|
-
return cachedShellConfig;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
|
129
|
-
const bashOnPath = findBashOnPath();
|
|
130
|
-
if (bashOnPath) {
|
|
131
|
-
cachedShellConfig = buildConfig(bashOnPath);
|
|
132
|
-
return cachedShellConfig;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
throw new Error(
|
|
136
|
-
`No bash shell found. Options:\n` +
|
|
137
|
-
` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
|
|
138
|
-
` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
|
|
139
|
-
` 3. Set shellPath in ~/.omp/agent/settings.json\n\n` +
|
|
140
|
-
`Searched Git Bash in:\n${paths.map(p => ` ${p}`).join("\n")}`,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Unix: prefer user's shell from $SHELL if it's bash/zsh and executable
|
|
145
|
-
const userShell = process.env.SHELL;
|
|
146
|
-
const isValidShell = userShell && (userShell.includes("bash") || userShell.includes("zsh"));
|
|
147
|
-
if (isValidShell && (await isExecutable(userShell))) {
|
|
148
|
-
cachedShellConfig = buildConfig(userShell);
|
|
149
|
-
return cachedShellConfig;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Fallback paths (Claude's approach: check known locations)
|
|
153
|
-
const fallbackPaths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"];
|
|
154
|
-
const preferZsh = !userShell?.includes("bash");
|
|
155
|
-
const shellOrder = preferZsh ? ["zsh", "bash"] : ["bash", "zsh"];
|
|
156
|
-
|
|
157
|
-
for (const shellName of shellOrder) {
|
|
158
|
-
for (const dir of fallbackPaths) {
|
|
159
|
-
const shellPath = `${dir}/${shellName}`;
|
|
160
|
-
if (await isExecutable(shellPath)) {
|
|
161
|
-
cachedShellConfig = buildConfig(shellPath);
|
|
162
|
-
return cachedShellConfig;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Last resort: use Bun.which
|
|
168
|
-
const bashPath = Bun.which("bash");
|
|
169
|
-
if (bashPath) {
|
|
170
|
-
cachedShellConfig = buildConfig(bashPath);
|
|
171
|
-
return cachedShellConfig;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const shPath = Bun.which("sh");
|
|
175
|
-
cachedShellConfig = buildConfig(shPath || "sh");
|
|
176
|
-
return cachedShellConfig;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
let pgrepAvailable: string | null | undefined;
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Check if pgrep is available on this system (cached).
|
|
183
|
-
*/
|
|
184
|
-
function hasPgrep(): string | null {
|
|
185
|
-
if (pgrepAvailable === undefined) {
|
|
186
|
-
try {
|
|
187
|
-
pgrepAvailable = Bun.which("pgrep") ?? null;
|
|
188
|
-
} catch {
|
|
189
|
-
pgrepAvailable = null;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return pgrepAvailable;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Get direct children of a PID using pgrep.
|
|
197
|
-
*/
|
|
198
|
-
async function getChildrenViaPgrep(pid: number): Promise<number[]> {
|
|
199
|
-
const result = await $`pgrep -P ${pid}`.quiet().nothrow();
|
|
200
|
-
if (result.exitCode !== 0) return [];
|
|
201
|
-
const output = result.stdout.toString().trim();
|
|
202
|
-
if (!output) return [];
|
|
203
|
-
|
|
204
|
-
const children: number[] = [];
|
|
205
|
-
for (const line of output.split("\n")) {
|
|
206
|
-
const childPid = parseInt(line, 10);
|
|
207
|
-
if (!Number.isNaN(childPid)) children.push(childPid);
|
|
208
|
-
}
|
|
209
|
-
return children;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Get direct children of a PID using /proc (Linux only).
|
|
214
|
-
*/
|
|
215
|
-
async function getChildrenViaProc(pid: number): Promise<number[]> {
|
|
216
|
-
try {
|
|
217
|
-
const script = `for p in /proc/[0-9]*/stat; do cat "$p" 2>/dev/null; done | awk -v ppid=${pid} '$4 == ppid { print $1 }'`;
|
|
218
|
-
const result = await $`sh -c ${script}`.quiet().nothrow();
|
|
219
|
-
if (result.exitCode !== 0) return [];
|
|
220
|
-
const output = result.stdout.toString().trim();
|
|
221
|
-
if (!output) return [];
|
|
222
|
-
|
|
223
|
-
const children: number[] = [];
|
|
224
|
-
for (const line of output.split("\n")) {
|
|
225
|
-
const childPid = parseInt(line, 10);
|
|
226
|
-
if (!Number.isNaN(childPid)) children.push(childPid);
|
|
227
|
-
}
|
|
228
|
-
return children;
|
|
229
|
-
} catch {
|
|
230
|
-
return [];
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Collect all descendant PIDs breadth-first.
|
|
236
|
-
* Returns deepest descendants first (reverse BFS order) for proper kill ordering.
|
|
237
|
-
*/
|
|
238
|
-
async function getDescendantPids(pid: number): Promise<number[]> {
|
|
239
|
-
const getChildren = hasPgrep() ? getChildrenViaPgrep : getChildrenViaProc;
|
|
240
|
-
const descendants: number[] = [];
|
|
241
|
-
const queue = [pid];
|
|
242
|
-
|
|
243
|
-
while (queue.length > 0) {
|
|
244
|
-
const current = queue.shift()!;
|
|
245
|
-
const children = await getChildren(current);
|
|
246
|
-
for (const child of children) {
|
|
247
|
-
descendants.push(child);
|
|
248
|
-
queue.push(child);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Reverse so deepest children are killed first
|
|
253
|
-
return descendants.reverse();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function tryKill(pid: number, signal: NodeJS.Signals): boolean {
|
|
257
|
-
try {
|
|
258
|
-
process.kill(pid, signal);
|
|
259
|
-
return true;
|
|
260
|
-
} catch {
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Kill a process and all its descendants.
|
|
267
|
-
* @param gracePeriodMs - Time to wait after SIGTERM before SIGKILL (0 = immediate SIGKILL)
|
|
268
|
-
*/
|
|
269
|
-
export async function killProcessTree(pid: number, gracePeriodMs = 0): Promise<void> {
|
|
270
|
-
if (process.platform === "win32") {
|
|
271
|
-
await $`taskkill /F /T /PID ${pid}`.quiet().nothrow();
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const signal = gracePeriodMs > 0 ? "SIGTERM" : "SIGKILL";
|
|
276
|
-
|
|
277
|
-
// Fast path: process group kill (works if pid is group leader)
|
|
278
|
-
try {
|
|
279
|
-
process.kill(-pid, signal);
|
|
280
|
-
if (gracePeriodMs > 0) {
|
|
281
|
-
await Bun.sleep(gracePeriodMs);
|
|
282
|
-
try {
|
|
283
|
-
process.kill(-pid, "SIGKILL");
|
|
284
|
-
} catch {
|
|
285
|
-
// Already dead
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return;
|
|
289
|
-
} catch {
|
|
290
|
-
// Not a process group leader, fall through
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Collect descendants BEFORE killing to minimize race window
|
|
294
|
-
const allPids = [...(await getDescendantPids(pid)), pid];
|
|
295
|
-
|
|
296
|
-
if (gracePeriodMs > 0) {
|
|
297
|
-
for (const p of allPids) tryKill(p, "SIGTERM");
|
|
298
|
-
await Bun.sleep(gracePeriodMs);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
for (const p of allPids) tryKill(p, "SIGKILL");
|
|
302
|
-
}
|