@oh-my-pi/pi-coding-agent 9.4.0 → 9.6.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 +84 -0
- package/package.json +9 -8
- package/src/capability/index.ts +7 -9
- package/src/cli/config-cli.ts +86 -73
- package/src/cli/update-cli.ts +45 -3
- package/src/commit/agentic/agent.ts +4 -4
- package/src/commit/agentic/index.ts +6 -5
- package/src/commit/agentic/tools/analyze-file.ts +5 -7
- package/src/commit/agentic/tools/index.ts +3 -3
- package/src/commit/model-selection.ts +13 -17
- package/src/commit/pipeline.ts +5 -5
- package/src/config/model-registry.ts +7 -0
- package/src/config/settings-schema.ts +836 -0
- package/src/config/settings.ts +702 -0
- package/src/discovery/helpers.ts +55 -11
- package/src/exa/index.ts +1 -1
- package/src/exec/bash-executor.ts +13 -13
- package/src/exec/shell-session.ts +15 -3
- package/src/export/ttsr.ts +1 -1
- package/src/extensibility/skills.ts +40 -9
- package/src/index.ts +2 -10
- package/src/ipy/gateway-coordinator.ts +5 -143
- package/src/ipy/kernel.ts +6 -171
- package/src/ipy/runtime.ts +198 -0
- package/src/lsp/client.ts +14 -1
- package/src/lsp/defaults.json +0 -6
- package/src/lsp/index.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +26 -48
- package/src/modes/components/extensions/extension-dashboard.ts +22 -11
- package/src/modes/components/index.ts +1 -1
- package/src/modes/components/model-selector.ts +7 -7
- package/src/modes/components/settings-defs.ts +210 -915
- package/src/modes/components/settings-selector.ts +80 -106
- package/src/modes/components/status-line/types.ts +2 -8
- package/src/modes/components/status-line-segment-editor.ts +1 -1
- package/src/modes/components/status-line.ts +26 -3
- package/src/modes/controllers/event-controller.ts +9 -8
- package/src/modes/controllers/input-controller.ts +19 -15
- package/src/modes/controllers/selector-controller.ts +30 -14
- package/src/modes/interactive-mode.ts +10 -10
- package/src/modes/rpc/rpc-mode.ts +10 -0
- package/src/modes/rpc/rpc-types.ts +3 -0
- package/src/modes/types.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +4 -3
- package/src/patch/index.ts +7 -7
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/bash.md +12 -2
- package/src/prompts/tools/task.md +180 -73
- package/src/sdk.ts +38 -61
- package/src/session/agent-session.ts +66 -55
- package/src/session/agent-storage.ts +1 -1
- package/src/session/session-manager.ts +10 -10
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +9 -9
- package/src/task/index.ts +2 -2
- package/src/tools/ask.ts +5 -6
- package/src/tools/bash-interceptor.ts +39 -1
- package/src/tools/bash-normalize.ts +126 -0
- package/src/tools/bash.ts +31 -5
- package/src/tools/find.ts +51 -33
- package/src/tools/index.ts +5 -23
- package/src/tools/plan-mode-guard.ts +1 -6
- package/src/tools/python.ts +2 -2
- package/src/tools/read.ts +2 -2
- package/src/tools/write.ts +2 -2
- package/src/utils/ignore-files.ts +119 -0
- package/src/web/search/providers/perplexity.ts +1 -1
- package/examples/sdk/10-settings.ts +0 -37
- package/src/config/settings-manager.ts +0 -2015
package/src/discovery/helpers.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { readDirEntries, readFile } from "../capability/fs";
|
|
|
8
8
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
9
9
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
10
10
|
import { parseFrontmatter } from "../utils/frontmatter";
|
|
11
|
+
import { addIgnoreRules, createIgnoreMatcher, type IgnoreMatcher, shouldIgnore } from "../utils/ignore-files";
|
|
11
12
|
|
|
12
13
|
const VALID_THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
13
14
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
@@ -236,6 +237,10 @@ export async function loadSkillsFromDir(
|
|
|
236
237
|
const warnings: string[] = [];
|
|
237
238
|
const { dir, level, providerId, requireDescription = false } = options;
|
|
238
239
|
|
|
240
|
+
// Initialize ignore matcher and read ignore rules from root
|
|
241
|
+
const ig = createIgnoreMatcher();
|
|
242
|
+
await addIgnoreRules(ig, dir, dir, readFile);
|
|
243
|
+
|
|
239
244
|
const entries = await readDirEntries(dir);
|
|
240
245
|
const skillDirs = entries.filter(
|
|
241
246
|
entry => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules",
|
|
@@ -243,7 +248,14 @@ export async function loadSkillsFromDir(
|
|
|
243
248
|
|
|
244
249
|
const results = await Promise.all(
|
|
245
250
|
skillDirs.map(async entry => {
|
|
246
|
-
const
|
|
251
|
+
const entryPath = path.join(dir, entry.name);
|
|
252
|
+
|
|
253
|
+
// Check if this directory should be ignored
|
|
254
|
+
if (shouldIgnore(ig, dir, entryPath, true)) {
|
|
255
|
+
return { item: null as Skill | null, warning: null as string | null };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const skillFile = path.join(entryPath, "SKILL.md");
|
|
247
259
|
const content = await readFile(skillFile);
|
|
248
260
|
if (!content) {
|
|
249
261
|
return { item: null as Skill | null, warning: null as string | null };
|
|
@@ -311,6 +323,7 @@ export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>):
|
|
|
311
323
|
|
|
312
324
|
/**
|
|
313
325
|
* Load files from a directory matching a pattern.
|
|
326
|
+
* Respects .gitignore, .ignore, and .fdignore files.
|
|
314
327
|
*/
|
|
315
328
|
export async function loadFilesFromDir<T>(
|
|
316
329
|
_ctx: LoadContext,
|
|
@@ -324,24 +337,47 @@ export async function loadFilesFromDir<T>(
|
|
|
324
337
|
transform: (name: string, content: string, path: string, source: SourceMeta) => T | null;
|
|
325
338
|
/** Whether to recurse into subdirectories */
|
|
326
339
|
recursive?: boolean;
|
|
340
|
+
/** Root directory for ignore file handling (defaults to dir) */
|
|
341
|
+
rootDir?: string;
|
|
342
|
+
/** Ignore matcher (used internally for recursion) */
|
|
343
|
+
ignoreMatcher?: IgnoreMatcher;
|
|
327
344
|
},
|
|
328
345
|
): Promise<LoadResult<T>> {
|
|
346
|
+
const rootDir = options.rootDir ?? dir;
|
|
347
|
+
const ig = options.ignoreMatcher ?? createIgnoreMatcher();
|
|
348
|
+
|
|
349
|
+
// Read ignore rules from this directory
|
|
350
|
+
await addIgnoreRules(ig, dir, rootDir, readFile);
|
|
351
|
+
|
|
329
352
|
const entries = await readDirEntries(dir);
|
|
330
353
|
|
|
331
354
|
const visibleEntries = entries.filter(entry => !entry.name.startsWith("."));
|
|
332
355
|
|
|
333
|
-
const directories = options.recursive
|
|
356
|
+
const directories = options.recursive
|
|
357
|
+
? visibleEntries.filter(entry => {
|
|
358
|
+
if (!entry.isDirectory()) return false;
|
|
359
|
+
const entryPath = path.join(dir, entry.name);
|
|
360
|
+
return !shouldIgnore(ig, rootDir, entryPath, true);
|
|
361
|
+
})
|
|
362
|
+
: [];
|
|
334
363
|
|
|
335
|
-
const files = visibleEntries
|
|
336
|
-
|
|
337
|
-
.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
});
|
|
364
|
+
const files = visibleEntries.filter(entry => {
|
|
365
|
+
if (!entry.isFile()) return false;
|
|
366
|
+
const entryPath = path.join(dir, entry.name);
|
|
367
|
+
if (shouldIgnore(ig, rootDir, entryPath, false)) return false;
|
|
368
|
+
if (!options.extensions) return true;
|
|
369
|
+
return options.extensions.some(ext => entry.name.endsWith(`.${ext}`));
|
|
370
|
+
});
|
|
341
371
|
|
|
342
372
|
const [subResults, fileResults] = await Promise.all([
|
|
343
373
|
Promise.all(
|
|
344
|
-
directories.map(entry =>
|
|
374
|
+
directories.map(entry =>
|
|
375
|
+
loadFilesFromDir(_ctx, path.join(dir, entry.name), provider, level, {
|
|
376
|
+
...options,
|
|
377
|
+
rootDir,
|
|
378
|
+
ignoreMatcher: ig,
|
|
379
|
+
}),
|
|
380
|
+
),
|
|
345
381
|
),
|
|
346
382
|
Promise.all(
|
|
347
383
|
files.map(async entry => {
|
|
@@ -435,24 +471,32 @@ function isExtensionModuleFile(name: string): boolean {
|
|
|
435
471
|
* 3. Subdirectory with package.json: `extensions/<ext>/package.json` with "omp"/"pi" field → load declared paths
|
|
436
472
|
*
|
|
437
473
|
* No recursion beyond one level. Complex packages must use package.json manifest.
|
|
474
|
+
* Respects .gitignore, .ignore, and .fdignore files.
|
|
438
475
|
*/
|
|
439
476
|
export async function discoverExtensionModulePaths(ctx: LoadContext, dir: string): Promise<string[]> {
|
|
440
477
|
const discovered: string[] = [];
|
|
441
478
|
const entries = await readDirEntries(dir);
|
|
442
479
|
|
|
480
|
+
// Initialize ignore matcher and read ignore rules from root
|
|
481
|
+
const ig = createIgnoreMatcher();
|
|
482
|
+
await addIgnoreRules(ig, dir, dir, readFile);
|
|
483
|
+
|
|
443
484
|
for (const entry of entries) {
|
|
444
485
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
445
486
|
|
|
446
487
|
const entryPath = path.join(dir, entry.name);
|
|
447
488
|
|
|
448
489
|
// 1. Direct files: *.ts or *.js
|
|
449
|
-
if (entry.isFile() && isExtensionModuleFile(entry.name)) {
|
|
490
|
+
if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionModuleFile(entry.name)) {
|
|
491
|
+
if (shouldIgnore(ig, dir, entryPath, false)) continue;
|
|
450
492
|
discovered.push(entryPath);
|
|
451
493
|
continue;
|
|
452
494
|
}
|
|
453
495
|
|
|
454
496
|
// 2 & 3. Subdirectories
|
|
455
|
-
if (entry.isDirectory()) {
|
|
497
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
498
|
+
if (shouldIgnore(ig, dir, entryPath, true)) continue;
|
|
499
|
+
|
|
456
500
|
const subEntries = await readDirEntries(entryPath);
|
|
457
501
|
const subFileNames = new Set(subEntries.filter(e => e.isFile()).map(e => e.name));
|
|
458
502
|
|
package/src/exa/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - 2 researcher tools (start, poll)
|
|
9
9
|
* - 14 websets tools (CRUD, items, search, enrichment, monitor)
|
|
10
10
|
*/
|
|
11
|
-
import type { ExaSettings } from "../config/settings
|
|
11
|
+
import type { ExaSettings } from "../config/settings";
|
|
12
12
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
13
13
|
import { companyTool } from "./company";
|
|
14
14
|
import { linkedinTool } from "./linkedin";
|
|
@@ -4,7 +4,7 @@
|
|
|
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 {
|
|
7
|
+
import { Settings } from "../config/settings";
|
|
8
8
|
import { OutputSink } from "../session/streaming-output";
|
|
9
9
|
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
10
10
|
import { executeShellCommand } from "./shell-session";
|
|
@@ -34,10 +34,11 @@ export interface BashResult {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
|
37
|
-
const
|
|
37
|
+
const settings = await Settings.init();
|
|
38
|
+
const { shell, args, env, prefix } = settings.getShellConfig();
|
|
38
39
|
const snapshotPath = await getOrCreateSnapshot(shell, env);
|
|
39
40
|
|
|
40
|
-
if (shouldUsePersistentShell(
|
|
41
|
+
if (shouldUsePersistentShell(settings.get("bash.persistentShell"))) {
|
|
41
42
|
return await executeShellCommand({ shell, env, prefix, snapshotPath }, command, {
|
|
42
43
|
cwd: options?.cwd,
|
|
43
44
|
timeout: options?.timeout,
|
|
@@ -52,19 +53,18 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
52
53
|
return await executeBashOnce(command, options, { shell, args, env, prefix, snapshotPath });
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Determine whether to use persistent shell sessions.
|
|
58
|
+
* Priority: OMP_SHELL_PERSIST env var > settings > default (false)
|
|
59
|
+
*/
|
|
60
|
+
function shouldUsePersistentShell(settingValue: boolean): boolean {
|
|
61
|
+
// Env var takes precedence (for debugging/override)
|
|
56
62
|
const flag = parseEnvFlag(process.env.OMP_SHELL_PERSIST);
|
|
57
63
|
if (flag !== undefined) return flag;
|
|
64
|
+
// Windows never uses persistent shell (too unreliable)
|
|
58
65
|
if (process.platform === "win32") return false;
|
|
59
|
-
|
|
60
|
-
return
|
|
61
|
-
normalized.includes("bash") ||
|
|
62
|
-
normalized.includes("zsh") ||
|
|
63
|
-
normalized.includes("fish") ||
|
|
64
|
-
normalized.endsWith("/sh") ||
|
|
65
|
-
normalized.endsWith("\\\\sh") ||
|
|
66
|
-
normalized.endsWith("sh")
|
|
67
|
-
);
|
|
66
|
+
// Use setting value (defaults to false)
|
|
67
|
+
return settingValue;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function parseEnvFlag(value: string | undefined): boolean | undefined {
|
|
@@ -194,6 +194,7 @@ class ShellSession {
|
|
|
194
194
|
#buffer = "";
|
|
195
195
|
#queue: Promise<void> = Promise.resolve();
|
|
196
196
|
#chunkQueue: Promise<void> = Promise.resolve();
|
|
197
|
+
#streamsDone: Promise<unknown> = Promise.resolve();
|
|
197
198
|
#current: RunningCommand | null = null;
|
|
198
199
|
#startPromise: Promise<void> | null = null;
|
|
199
200
|
#closed = false;
|
|
@@ -313,8 +314,7 @@ class ShellSession {
|
|
|
313
314
|
}
|
|
314
315
|
};
|
|
315
316
|
|
|
316
|
-
|
|
317
|
-
void readStream(child.stderr);
|
|
317
|
+
this.#streamsDone = Promise.allSettled([readStream(child.stdout), readStream(child.stderr)]);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
async #enqueueChunk(text: string): Promise<void> {
|
|
@@ -477,6 +477,11 @@ class ShellSession {
|
|
|
477
477
|
if (completed) return;
|
|
478
478
|
|
|
479
479
|
await this.#terminateSession();
|
|
480
|
+
|
|
481
|
+
// Drain streams and chunk queue - marker might have arrived but not yet processed
|
|
482
|
+
await this.#streamsDone;
|
|
483
|
+
await this.#chunkQueue;
|
|
484
|
+
|
|
480
485
|
if (running.completed) return;
|
|
481
486
|
running.completed = true;
|
|
482
487
|
running.abortListener?.();
|
|
@@ -522,14 +527,21 @@ class ShellSession {
|
|
|
522
527
|
this.#child = null;
|
|
523
528
|
this.#stdinWriter = null;
|
|
524
529
|
this.#startPromise = null;
|
|
525
|
-
this.#buffer = "";
|
|
526
530
|
|
|
527
531
|
if (!running || running.completed) return;
|
|
532
|
+
|
|
533
|
+
// Wait for any pending chunks to be processed - marker might be in the queue
|
|
534
|
+
await this.#streamsDone;
|
|
535
|
+
await this.#chunkQueue;
|
|
536
|
+
|
|
537
|
+
if (running.completed) return;
|
|
538
|
+
|
|
528
539
|
running.cancelled = true;
|
|
529
540
|
running.abortReason = "signal";
|
|
530
541
|
running.completed = true;
|
|
531
542
|
running.abortListener?.();
|
|
532
543
|
this.#current = null;
|
|
544
|
+
this.#buffer = "";
|
|
533
545
|
const summary = await running.sink.dump(running.abortNotice ?? "Shell session terminated");
|
|
534
546
|
running.resolve({
|
|
535
547
|
exitCode: undefined,
|
package/src/export/ttsr.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type { Rule } from "../capability/rule";
|
|
10
|
-
import type { TtsrSettings } from "../config/settings
|
|
10
|
+
import type { TtsrSettings } from "../config/settings";
|
|
11
11
|
|
|
12
12
|
interface TtsrEntry {
|
|
13
13
|
rule: Rule;
|
|
@@ -3,10 +3,11 @@ import * as path from "node:path";
|
|
|
3
3
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { skillCapability } from "../capability/skill";
|
|
5
5
|
import type { SourceMeta } from "../capability/types";
|
|
6
|
-
import type { SkillsSettings } from "../config/settings
|
|
6
|
+
import type { SkillsSettings } from "../config/settings";
|
|
7
7
|
import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
|
|
8
8
|
import { loadCapability } from "../discovery";
|
|
9
9
|
import { parseFrontmatter } from "../utils/frontmatter";
|
|
10
|
+
import { addIgnoreRules, createIgnoreMatcher, type IgnoreMatcher, shouldIgnore } from "../utils/ignore-files";
|
|
10
11
|
|
|
11
12
|
// Re-export SkillFrontmatter for backward compatibility
|
|
12
13
|
export type { ImportedSkillFrontmatter as SkillFrontmatter };
|
|
@@ -38,14 +39,24 @@ export interface LoadSkillsFromDirOptions {
|
|
|
38
39
|
source: string;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
async function readFileContent(filePath: string): Promise<string | null> {
|
|
43
|
+
try {
|
|
44
|
+
return await fs.readFile(filePath, "utf-8");
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
/**
|
|
42
51
|
* Load skills from a directory recursively.
|
|
43
52
|
* Skills are directories containing a SKILL.md file with frontmatter including a description.
|
|
53
|
+
* Respects .gitignore, .ignore, and .fdignore files.
|
|
44
54
|
*/
|
|
45
55
|
export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Promise<LoadSkillsResult> {
|
|
46
56
|
const skills: Skill[] = [];
|
|
47
57
|
const warnings: SkillWarning[] = [];
|
|
48
58
|
const seenPaths = new Set<string>();
|
|
59
|
+
const rootDir = options.dir;
|
|
49
60
|
|
|
50
61
|
async function addSkill(skillFile: string, skillDir: string, dirName: string): Promise<void> {
|
|
51
62
|
if (seenPaths.has(skillFile)) return;
|
|
@@ -70,8 +81,11 @@ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Prom
|
|
|
70
81
|
}
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
async function scanDir(dir: string): Promise<void> {
|
|
84
|
+
async function scanDir(dir: string, ig: IgnoreMatcher): Promise<void> {
|
|
74
85
|
try {
|
|
86
|
+
// Add ignore rules from this directory
|
|
87
|
+
await addIgnoreRules(ig, dir, rootDir, readFileContent);
|
|
88
|
+
|
|
75
89
|
// First check if this directory itself is a skill
|
|
76
90
|
const selfSkillFile = path.join(dir, "SKILL.md");
|
|
77
91
|
try {
|
|
@@ -92,8 +106,13 @@ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Prom
|
|
|
92
106
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
93
107
|
|
|
94
108
|
const fullPath = path.join(dir, entry.name);
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
const isDir = entry.isDirectory();
|
|
110
|
+
|
|
111
|
+
// Check if this entry should be ignored
|
|
112
|
+
if (shouldIgnore(ig, rootDir, fullPath, isDir)) continue;
|
|
113
|
+
|
|
114
|
+
if (isDir) {
|
|
115
|
+
await scanDir(fullPath, ig);
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
} catch (err) {
|
|
@@ -101,7 +120,8 @@ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Prom
|
|
|
101
120
|
}
|
|
102
121
|
}
|
|
103
122
|
|
|
104
|
-
|
|
123
|
+
const ig = createIgnoreMatcher();
|
|
124
|
+
await scanDir(options.dir, ig);
|
|
105
125
|
|
|
106
126
|
return { skills, warnings };
|
|
107
127
|
}
|
|
@@ -109,11 +129,13 @@ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Prom
|
|
|
109
129
|
/**
|
|
110
130
|
* Scan a directory for SKILL.md files recursively.
|
|
111
131
|
* Used internally by loadSkills for custom directories.
|
|
132
|
+
* Respects .gitignore, .ignore, and .fdignore files.
|
|
112
133
|
*/
|
|
113
134
|
async function scanDirectoryForSkills(dir: string): Promise<LoadSkillsResult> {
|
|
114
135
|
const skills: Skill[] = [];
|
|
115
136
|
const warnings: SkillWarning[] = [];
|
|
116
137
|
const seenPaths = new Set<string>();
|
|
138
|
+
const rootDir = dir;
|
|
117
139
|
|
|
118
140
|
async function addSkill(skillFile: string, skillDir: string, dirName: string): Promise<void> {
|
|
119
141
|
if (seenPaths.has(skillFile)) return;
|
|
@@ -138,8 +160,11 @@ async function scanDirectoryForSkills(dir: string): Promise<LoadSkillsResult> {
|
|
|
138
160
|
}
|
|
139
161
|
}
|
|
140
162
|
|
|
141
|
-
async function scanDir(currentDir: string): Promise<void> {
|
|
163
|
+
async function scanDir(currentDir: string, ig: IgnoreMatcher): Promise<void> {
|
|
142
164
|
try {
|
|
165
|
+
// Add ignore rules from this directory
|
|
166
|
+
await addIgnoreRules(ig, currentDir, rootDir, readFileContent);
|
|
167
|
+
|
|
143
168
|
// First check if this directory itself is a skill
|
|
144
169
|
const selfSkillFile = path.join(currentDir, "SKILL.md");
|
|
145
170
|
try {
|
|
@@ -160,8 +185,13 @@ async function scanDirectoryForSkills(dir: string): Promise<LoadSkillsResult> {
|
|
|
160
185
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
161
186
|
|
|
162
187
|
const fullPath = path.join(currentDir, entry.name);
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
const isDir = entry.isDirectory();
|
|
189
|
+
|
|
190
|
+
// Check if this entry should be ignored
|
|
191
|
+
if (shouldIgnore(ig, rootDir, fullPath, isDir)) continue;
|
|
192
|
+
|
|
193
|
+
if (isDir) {
|
|
194
|
+
await scanDir(fullPath, ig);
|
|
165
195
|
}
|
|
166
196
|
}
|
|
167
197
|
} catch (err) {
|
|
@@ -169,7 +199,8 @@ async function scanDirectoryForSkills(dir: string): Promise<LoadSkillsResult> {
|
|
|
169
199
|
}
|
|
170
200
|
}
|
|
171
201
|
|
|
172
|
-
|
|
202
|
+
const ig = createIgnoreMatcher();
|
|
203
|
+
await scanDir(dir, ig);
|
|
173
204
|
|
|
174
205
|
return { skills, warnings };
|
|
175
206
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,15 +12,8 @@ export { formatKeyHint, formatKeyHints } from "./config/keybindings";
|
|
|
12
12
|
export { ModelRegistry } from "./config/model-registry";
|
|
13
13
|
// Prompt templates
|
|
14
14
|
export type { PromptTemplate } from "./config/prompt-templates";
|
|
15
|
-
export {
|
|
16
|
-
|
|
17
|
-
type ImageSettings,
|
|
18
|
-
type LspSettings,
|
|
19
|
-
type RetrySettings,
|
|
20
|
-
type Settings,
|
|
21
|
-
SettingsManager,
|
|
22
|
-
type SkillsSettings,
|
|
23
|
-
} from "./config/settings-manager";
|
|
15
|
+
export type { CompactionSettings, RetrySettings, SkillsSettings } from "./config/settings";
|
|
16
|
+
export { Settings, settings } from "./config/settings";
|
|
24
17
|
// Custom commands
|
|
25
18
|
export type {
|
|
26
19
|
CustomCommand,
|
|
@@ -188,7 +181,6 @@ export {
|
|
|
188
181
|
FindTool,
|
|
189
182
|
GrepTool,
|
|
190
183
|
LsTool,
|
|
191
|
-
loadSettings,
|
|
192
184
|
loadSshTool,
|
|
193
185
|
PythonTool,
|
|
194
186
|
ReadTool,
|
|
@@ -4,9 +4,10 @@ import * as path from "node:path";
|
|
|
4
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 { Settings } from "../config/settings";
|
|
8
8
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
9
9
|
import { time } from "../utils/timings";
|
|
10
|
+
import { filterEnv, resolvePythonRuntime } from "./runtime";
|
|
10
11
|
|
|
11
12
|
const GATEWAY_DIR_NAME = "python-gateway";
|
|
12
13
|
const GATEWAY_INFO_FILE = "gateway.json";
|
|
@@ -18,96 +19,6 @@ const GATEWAY_LOCK_STALE_MS = GATEWAY_STARTUP_TIMEOUT_MS * 2;
|
|
|
18
19
|
const GATEWAY_LOCK_HEARTBEAT_MS = 5000;
|
|
19
20
|
const HEALTH_CHECK_TIMEOUT_MS = 3000;
|
|
20
21
|
|
|
21
|
-
const DEFAULT_ENV_ALLOWLIST = new Set([
|
|
22
|
-
"PATH",
|
|
23
|
-
"HOME",
|
|
24
|
-
"USER",
|
|
25
|
-
"LOGNAME",
|
|
26
|
-
"SHELL",
|
|
27
|
-
"LANG",
|
|
28
|
-
"LC_ALL",
|
|
29
|
-
"LC_CTYPE",
|
|
30
|
-
"LC_MESSAGES",
|
|
31
|
-
"TERM",
|
|
32
|
-
"TERM_PROGRAM",
|
|
33
|
-
"TERM_PROGRAM_VERSION",
|
|
34
|
-
"TMPDIR",
|
|
35
|
-
"TEMP",
|
|
36
|
-
"TMP",
|
|
37
|
-
"XDG_CACHE_HOME",
|
|
38
|
-
"XDG_CONFIG_HOME",
|
|
39
|
-
"XDG_DATA_HOME",
|
|
40
|
-
"XDG_RUNTIME_DIR",
|
|
41
|
-
"SSH_AUTH_SOCK",
|
|
42
|
-
"SSH_AGENT_PID",
|
|
43
|
-
"CONDA_PREFIX",
|
|
44
|
-
"CONDA_DEFAULT_ENV",
|
|
45
|
-
"VIRTUAL_ENV",
|
|
46
|
-
"PYTHONPATH",
|
|
47
|
-
"SYSTEMROOT",
|
|
48
|
-
"COMSPEC",
|
|
49
|
-
"WINDIR",
|
|
50
|
-
"USERPROFILE",
|
|
51
|
-
"LOCALAPPDATA",
|
|
52
|
-
"APPDATA",
|
|
53
|
-
"PROGRAMDATA",
|
|
54
|
-
"PATHEXT",
|
|
55
|
-
"USERNAME",
|
|
56
|
-
"HOMEDRIVE",
|
|
57
|
-
"HOMEPATH",
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
const WINDOWS_ENV_ALLOWLIST = new Set([
|
|
61
|
-
"APPDATA",
|
|
62
|
-
"COMPUTERNAME",
|
|
63
|
-
"COMSPEC",
|
|
64
|
-
"HOMEDRIVE",
|
|
65
|
-
"HOMEPATH",
|
|
66
|
-
"LOCALAPPDATA",
|
|
67
|
-
"NUMBER_OF_PROCESSORS",
|
|
68
|
-
"OS",
|
|
69
|
-
"PATH",
|
|
70
|
-
"PATHEXT",
|
|
71
|
-
"PROCESSOR_ARCHITECTURE",
|
|
72
|
-
"PROCESSOR_IDENTIFIER",
|
|
73
|
-
"PROGRAMDATA",
|
|
74
|
-
"PROGRAMFILES",
|
|
75
|
-
"PROGRAMFILES(X86)",
|
|
76
|
-
"PROGRAMW6432",
|
|
77
|
-
"SESSIONNAME",
|
|
78
|
-
"SYSTEMDRIVE",
|
|
79
|
-
"SYSTEMROOT",
|
|
80
|
-
"TEMP",
|
|
81
|
-
"TMP",
|
|
82
|
-
"USERDOMAIN",
|
|
83
|
-
"USERDOMAIN_ROAMINGPROFILE",
|
|
84
|
-
"USERPROFILE",
|
|
85
|
-
"USERNAME",
|
|
86
|
-
"WINDIR",
|
|
87
|
-
]);
|
|
88
|
-
|
|
89
|
-
const DEFAULT_ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "OMP_"];
|
|
90
|
-
|
|
91
|
-
const CASE_INSENSITIVE_ENV = process.platform === "win32";
|
|
92
|
-
const ACTIVE_ENV_ALLOWLIST = CASE_INSENSITIVE_ENV ? WINDOWS_ENV_ALLOWLIST : DEFAULT_ENV_ALLOWLIST;
|
|
93
|
-
|
|
94
|
-
const NORMALIZED_ALLOWLIST = new Map(
|
|
95
|
-
Array.from(ACTIVE_ENV_ALLOWLIST, key => [CASE_INSENSITIVE_ENV ? key.toUpperCase() : key, key] as const),
|
|
96
|
-
);
|
|
97
|
-
const NORMALIZED_ALLOW_PREFIXES = CASE_INSENSITIVE_ENV
|
|
98
|
-
? DEFAULT_ENV_ALLOW_PREFIXES.map(prefix => prefix.toUpperCase())
|
|
99
|
-
: DEFAULT_ENV_ALLOW_PREFIXES;
|
|
100
|
-
|
|
101
|
-
function normalizeEnvKey(key: string): string {
|
|
102
|
-
return CASE_INSENSITIVE_ENV ? key.toUpperCase() : key;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function resolvePathKey(env: Record<string, string | undefined>): string {
|
|
106
|
-
if (!CASE_INSENSITIVE_ENV) return "PATH";
|
|
107
|
-
const match = Object.keys(env).find(candidate => candidate.toLowerCase() === "path");
|
|
108
|
-
return match ?? "PATH";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
22
|
export interface GatewayInfo {
|
|
112
23
|
url: string;
|
|
113
24
|
pid: number;
|
|
@@ -130,56 +41,6 @@ let localGatewayProcess: Subprocess | null = null;
|
|
|
130
41
|
let localGatewayUrl: string | null = null;
|
|
131
42
|
let isCoordinatorInitialized = false;
|
|
132
43
|
|
|
133
|
-
function filterEnv(env: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
134
|
-
const filtered: Record<string, string | undefined> = {};
|
|
135
|
-
for (const [key, value] of Object.entries(env)) {
|
|
136
|
-
if (value === undefined) continue;
|
|
137
|
-
const normalizedKey = normalizeEnvKey(key);
|
|
138
|
-
const canonicalKey = NORMALIZED_ALLOWLIST.get(normalizedKey);
|
|
139
|
-
if (canonicalKey !== undefined) {
|
|
140
|
-
filtered[canonicalKey] = value;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (NORMALIZED_ALLOW_PREFIXES.some(prefix => normalizedKey.startsWith(prefix))) {
|
|
144
|
-
filtered[key] = value;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return filtered;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function resolveVenvPath(cwd: string): string | null {
|
|
151
|
-
if (process.env.VIRTUAL_ENV) return process.env.VIRTUAL_ENV;
|
|
152
|
-
const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
|
|
153
|
-
for (const candidate of candidates) {
|
|
154
|
-
if (fs.existsSync(candidate)) {
|
|
155
|
-
return candidate;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string | undefined>) {
|
|
162
|
-
const env = { ...baseEnv };
|
|
163
|
-
const venvPath = env.VIRTUAL_ENV ?? resolveVenvPath(cwd);
|
|
164
|
-
if (venvPath) {
|
|
165
|
-
env.VIRTUAL_ENV = venvPath;
|
|
166
|
-
const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
|
|
167
|
-
const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
|
|
168
|
-
if (fs.existsSync(pythonCandidate)) {
|
|
169
|
-
const pathKey = resolvePathKey(env);
|
|
170
|
-
const currentPath = env[pathKey];
|
|
171
|
-
env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
|
|
172
|
-
return { pythonPath: pythonCandidate, env, venvPath };
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const pythonPath = Bun.which("python") ?? Bun.which("python3");
|
|
177
|
-
if (!pythonPath) {
|
|
178
|
-
throw new Error("Python executable not found on PATH");
|
|
179
|
-
}
|
|
180
|
-
return { pythonPath, env, venvPath: null };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
44
|
async function allocatePort(): Promise<number> {
|
|
184
45
|
const { promise, resolve, reject } = Promise.withResolvers<number>();
|
|
185
46
|
const server = createServer();
|
|
@@ -368,7 +229,8 @@ async function isGatewayAlive(info: GatewayInfo): Promise<boolean> {
|
|
|
368
229
|
async function startGatewayProcess(
|
|
369
230
|
cwd: string,
|
|
370
231
|
): Promise<{ url: string; pid: number; pythonPath: string; venvPath: string | null }> {
|
|
371
|
-
const
|
|
232
|
+
const settings = await Settings.init();
|
|
233
|
+
const { shell, env } = settings.getShellConfig();
|
|
372
234
|
const filteredEnv = filterEnv(env);
|
|
373
235
|
const runtime = await resolvePythonRuntime(cwd, filteredEnv);
|
|
374
236
|
const snapshotPath = await getOrCreateSnapshot(shell, env).catch((err: unknown) => {
|
|
@@ -389,7 +251,7 @@ async function startGatewayProcess(
|
|
|
389
251
|
|
|
390
252
|
const gatewayProcess = Bun.spawn(
|
|
391
253
|
[
|
|
392
|
-
runtime.
|
|
254
|
+
runtime.pythonwPath,
|
|
393
255
|
"-m",
|
|
394
256
|
"kernel_gateway",
|
|
395
257
|
"--KernelGatewayApp.ip=127.0.0.1",
|