@oh-my-pi/pi-coding-agent 3.32.0 → 3.34.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 +49 -9
- package/README.md +12 -0
- package/docs/custom-tools.md +1 -1
- package/docs/extensions.md +4 -4
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +4 -8
- package/examples/custom-tools/README.md +2 -2
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/todo.ts +1 -1
- package/examples/hooks/custom-compaction.ts +4 -2
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +5 -5
- package/src/capability/ssh.ts +42 -0
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +21 -6
- package/src/core/auth-storage.ts +1 -1
- package/src/core/compaction/branch-summarization.ts +2 -2
- package/src/core/compaction/compaction.ts +2 -2
- package/src/core/compaction/utils.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -1
- package/src/core/file-mentions.ts +147 -5
- package/src/core/hooks/runner.ts +2 -2
- package/src/core/hooks/types.ts +1 -1
- package/src/core/index.ts +11 -0
- package/src/core/messages.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/model-resolver.ts +9 -4
- package/src/core/sdk.ts +26 -2
- package/src/core/session-manager.ts +3 -2
- package/src/core/settings-manager.ts +70 -0
- package/src/core/ssh/connection-manager.ts +466 -0
- package/src/core/ssh/ssh-executor.ts +190 -0
- package/src/core/ssh/sshfs-mount.ts +162 -0
- package/src/core/ssh-executor.ts +5 -0
- package/src/core/system-prompt.ts +424 -1
- package/src/core/title-generator.ts +109 -55
- package/src/core/tools/index.test.ts +1 -0
- package/src/core/tools/index.ts +3 -0
- package/src/core/tools/output.ts +37 -2
- package/src/core/tools/read.ts +24 -11
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/ssh.ts +302 -0
- package/src/core/tools/task/index.ts +1 -1
- package/src/core/tools/task/render.ts +10 -16
- package/src/core/tools/task/types.ts +1 -1
- package/src/core/tools/task/worker.ts +1 -1
- package/src/core/voice.ts +1 -1
- package/src/discovery/index.ts +3 -0
- package/src/discovery/ssh.ts +162 -0
- package/src/main.ts +2 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +9 -10
- package/src/modes/interactive/components/custom-message.ts +1 -1
- package/src/modes/interactive/components/footer.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +1 -1
- package/src/modes/interactive/components/oauth-selector.ts +1 -1
- package/src/modes/interactive/components/status-line.ts +1 -1
- package/src/modes/interactive/components/tree-selector.ts +9 -12
- package/src/modes/interactive/interactive-mode.ts +5 -2
- package/src/modes/interactive/theme/theme.ts +2 -2
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/prompts/system-prompt.md +4 -0
- package/src/prompts/tools/ssh.md +74 -0
- package/src/utils/image-resize.ts +1 -1
|
@@ -9,14 +9,148 @@
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import type { FileMentionMessage } from "./messages";
|
|
12
|
+
import { resolveReadPath } from "./tools/path-utils";
|
|
13
|
+
import { formatAge } from "./tools/render-utils";
|
|
14
|
+
import { DEFAULT_MAX_BYTES, formatSize, truncateHead, truncateStringToBytesFromStart } from "./tools/truncate";
|
|
12
15
|
|
|
13
16
|
/** Regex to match @filepath patterns in text */
|
|
14
|
-
const FILE_MENTION_REGEX = /@(
|
|
17
|
+
const FILE_MENTION_REGEX = /@([^\s@]+)/g;
|
|
18
|
+
const LEADING_PUNCTUATION_REGEX = /^[`"'([{<]+/;
|
|
19
|
+
const TRAILING_PUNCTUATION_REGEX = /[)\]}>.,;:!?"'`]+$/;
|
|
20
|
+
const MENTION_BOUNDARY_REGEX = /[\s([{<"'`]/;
|
|
21
|
+
const DEFAULT_DIR_LIMIT = 500;
|
|
22
|
+
|
|
23
|
+
function isMentionBoundary(text: string, index: number): boolean {
|
|
24
|
+
if (index === 0) return true;
|
|
25
|
+
return MENTION_BOUNDARY_REGEX.test(text[index - 1]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeMentionPath(rawPath: string): string | null {
|
|
29
|
+
let cleaned = rawPath.trim();
|
|
30
|
+
cleaned = cleaned.replace(LEADING_PUNCTUATION_REGEX, "");
|
|
31
|
+
cleaned = cleaned.replace(TRAILING_PUNCTUATION_REGEX, "");
|
|
32
|
+
cleaned = cleaned.trim();
|
|
33
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildTextOutput(textContent: string): { output: string; lineCount: number } {
|
|
37
|
+
const allLines = textContent.split("\n");
|
|
38
|
+
const totalFileLines = allLines.length;
|
|
39
|
+
const truncation = truncateHead(textContent);
|
|
40
|
+
|
|
41
|
+
if (truncation.firstLineExceedsLimit) {
|
|
42
|
+
const firstLine = allLines[0] ?? "";
|
|
43
|
+
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
44
|
+
const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
|
|
45
|
+
let outputText = snippet.text;
|
|
46
|
+
|
|
47
|
+
if (outputText.length > 0) {
|
|
48
|
+
outputText += `\n\n[Line 1 is ${formatSize(firstLineBytes)}, exceeds ${formatSize(
|
|
49
|
+
DEFAULT_MAX_BYTES,
|
|
50
|
+
)} limit. Showing first ${formatSize(snippet.bytes)} of the line.]`;
|
|
51
|
+
} else {
|
|
52
|
+
outputText = `[Line 1 is ${formatSize(firstLineBytes)}, exceeds ${formatSize(
|
|
53
|
+
DEFAULT_MAX_BYTES,
|
|
54
|
+
)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { output: outputText, lineCount: totalFileLines };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let outputText = truncation.content;
|
|
61
|
+
|
|
62
|
+
if (truncation.truncated) {
|
|
63
|
+
const endLineDisplay = truncation.outputLines;
|
|
64
|
+
const nextOffset = endLineDisplay + 1;
|
|
65
|
+
|
|
66
|
+
if (truncation.truncatedBy === "lines") {
|
|
67
|
+
outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
|
68
|
+
} else {
|
|
69
|
+
outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines} (${formatSize(
|
|
70
|
+
DEFAULT_MAX_BYTES,
|
|
71
|
+
)} limit). Use offset=${nextOffset} to continue]`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { output: outputText, lineCount: totalFileLines };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function buildDirectoryListing(absolutePath: string): Promise<{ output: string; lineCount: number }> {
|
|
79
|
+
let entries: string[];
|
|
80
|
+
try {
|
|
81
|
+
entries = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: absolutePath, dot: true, onlyFiles: false }));
|
|
82
|
+
} catch {
|
|
83
|
+
return { output: "(empty directory)", lineCount: 1 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
87
|
+
|
|
88
|
+
const results: string[] = [];
|
|
89
|
+
let entryLimitReached = false;
|
|
90
|
+
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (results.length >= DEFAULT_DIR_LIMIT) {
|
|
93
|
+
entryLimitReached = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fullPath = path.join(absolutePath, entry);
|
|
98
|
+
let suffix = "";
|
|
99
|
+
let age = "";
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const stat = await Bun.file(fullPath).stat();
|
|
103
|
+
if (stat.isDirectory()) {
|
|
104
|
+
suffix = "/";
|
|
105
|
+
}
|
|
106
|
+
const ageSeconds = Math.floor((Date.now() - stat.mtimeMs) / 1000);
|
|
107
|
+
age = formatAge(ageSeconds);
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const line = age ? `${entry}${suffix} (${age})` : `${entry}${suffix}`;
|
|
113
|
+
results.push(line);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (results.length === 0) {
|
|
117
|
+
return { output: "(empty directory)", lineCount: 1 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const rawOutput = results.join("\n");
|
|
121
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
122
|
+
let output = truncation.content;
|
|
123
|
+
|
|
124
|
+
const notices: string[] = [];
|
|
125
|
+
if (entryLimitReached) {
|
|
126
|
+
notices.push(`${DEFAULT_DIR_LIMIT} entries limit reached. Use limit=${DEFAULT_DIR_LIMIT * 2} for more`);
|
|
127
|
+
}
|
|
128
|
+
if (truncation.truncated) {
|
|
129
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
130
|
+
}
|
|
131
|
+
if (notices.length > 0) {
|
|
132
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { output, lineCount: output.split("\n").length };
|
|
136
|
+
}
|
|
15
137
|
|
|
16
138
|
/** Extract all @filepath mentions from text */
|
|
17
139
|
export function extractFileMentions(text: string): string[] {
|
|
18
140
|
const matches = [...text.matchAll(FILE_MENTION_REGEX)];
|
|
19
|
-
|
|
141
|
+
const mentions: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (const match of matches) {
|
|
144
|
+
const index = match.index ?? 0;
|
|
145
|
+
if (!isMentionBoundary(text, index)) continue;
|
|
146
|
+
|
|
147
|
+
const cleaned = sanitizeMentionPath(match[1]);
|
|
148
|
+
if (!cleaned) continue;
|
|
149
|
+
|
|
150
|
+
mentions.push(cleaned);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...new Set(mentions)];
|
|
20
154
|
}
|
|
21
155
|
|
|
22
156
|
/**
|
|
@@ -29,11 +163,19 @@ export async function generateFileMentionMessages(filePaths: string[], cwd: stri
|
|
|
29
163
|
const files: FileMentionMessage["files"] = [];
|
|
30
164
|
|
|
31
165
|
for (const filePath of filePaths) {
|
|
166
|
+
const absolutePath = resolveReadPath(filePath, cwd);
|
|
167
|
+
|
|
32
168
|
try {
|
|
33
|
-
const
|
|
169
|
+
const stat = await Bun.file(absolutePath).stat();
|
|
170
|
+
if (stat.isDirectory()) {
|
|
171
|
+
const { output, lineCount } = await buildDirectoryListing(absolutePath);
|
|
172
|
+
files.push({ path: filePath, content: output, lineCount });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
34
176
|
const content = await Bun.file(absolutePath).text();
|
|
35
|
-
const lineCount = content
|
|
36
|
-
files.push({ path: filePath, content, lineCount });
|
|
177
|
+
const { output, lineCount } = buildTextOutput(content);
|
|
178
|
+
files.push({ path: filePath, content: output, lineCount });
|
|
37
179
|
} catch {
|
|
38
180
|
// File doesn't exist or isn't readable - skip silently
|
|
39
181
|
}
|
package/src/core/hooks/runner.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Hook runner - executes hooks and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Model } from "@mariozechner/pi-ai";
|
|
6
5
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { theme } from "../../modes/interactive/theme/theme";
|
|
8
8
|
import type { ModelRegistry } from "../model-registry";
|
|
9
9
|
import type { SessionManager } from "../session-manager";
|
|
@@ -400,7 +400,7 @@ export class HookRunner {
|
|
|
400
400
|
*/
|
|
401
401
|
async emitBeforeAgentStart(
|
|
402
402
|
prompt: string,
|
|
403
|
-
images?: import("@
|
|
403
|
+
images?: import("@oh-my-pi/pi-ai").ImageContent[],
|
|
404
404
|
): Promise<BeforeAgentStartEventResult | undefined> {
|
|
405
405
|
const ctx = this.createContext();
|
|
406
406
|
let result: BeforeAgentStartEventResult | undefined;
|
package/src/core/hooks/types.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* and interact with the user via UI primitives.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
|
|
9
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
|
+
import type { ImageContent, Message, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import type { Component, TUI } from "@oh-my-pi/pi-tui";
|
|
11
11
|
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
12
12
|
import type { CompactionPreparation, CompactionResult } from "../compaction/index";
|
package/src/core/index.ts
CHANGED
|
@@ -38,5 +38,16 @@ export {
|
|
|
38
38
|
type MCPToolsLoadResult,
|
|
39
39
|
type MCPTransport,
|
|
40
40
|
} from "./mcp/index";
|
|
41
|
+
export {
|
|
42
|
+
buildRemoteCommand,
|
|
43
|
+
closeAllConnections,
|
|
44
|
+
closeConnection,
|
|
45
|
+
ensureConnection,
|
|
46
|
+
getControlDir,
|
|
47
|
+
getControlPathTemplate,
|
|
48
|
+
type SSHConnectionTarget,
|
|
49
|
+
} from "./ssh/connection-manager";
|
|
50
|
+
export { executeSSH, type SSHExecutorOptions, type SSHResult } from "./ssh/ssh-executor";
|
|
51
|
+
export { hasSshfs, isMounted, mountRemote, unmountAll, unmountRemote } from "./ssh/sshfs-mount";
|
|
41
52
|
|
|
42
53
|
export * as utils from "./utils";
|
package/src/core/messages.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* and provides a transformer to convert them to LLM-compatible messages.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai";
|
|
9
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
|
+
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
|
10
10
|
|
|
11
11
|
export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
|
|
12
12
|
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type KnownProvider,
|
|
12
12
|
type Model,
|
|
13
13
|
normalizeDomain,
|
|
14
|
-
} from "@
|
|
14
|
+
} from "@oh-my-pi/pi-ai";
|
|
15
15
|
import { type Static, Type } from "@sinclair/typebox";
|
|
16
16
|
import AjvModule from "ajv";
|
|
17
17
|
import type { AuthStorage } from "./auth-storage";
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Model resolution, scoping, and initial selection
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@mariozechner/pi-ai";
|
|
6
5
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { minimatch } from "minimatch";
|
|
9
9
|
import { isValidThinkingLevel } from "../cli/args";
|
|
@@ -25,6 +25,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|
|
25
25
|
cerebras: "zai-glm-4.6",
|
|
26
26
|
zai: "glm-4.6",
|
|
27
27
|
mistral: "devstral-medium-latest",
|
|
28
|
+
opencode: "claude-sonnet-4-5",
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export interface ScopedModel {
|
|
@@ -33,7 +34,7 @@ export interface ScopedModel {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/** Priority chain for auto-discovering smol/fast models */
|
|
36
|
-
export const SMOL_MODEL_PRIORITY = ["claude-haiku-4-5", "haiku", "flash", "mini"];
|
|
37
|
+
export const SMOL_MODEL_PRIORITY = ["cerebras/zai-glm-4.6", "claude-haiku-4-5", "haiku", "flash", "mini"];
|
|
37
38
|
|
|
38
39
|
/** Priority chain for auto-discovering slow/comprehensive models (reasoning, codex) */
|
|
39
40
|
export const SLOW_MODEL_PRIORITY = ["gpt-5.2-codex", "gpt-5.2", "codex", "gpt", "opus", "pro"];
|
|
@@ -443,12 +444,16 @@ export async function findSmolModel(
|
|
|
443
444
|
|
|
444
445
|
// 2. Try priority chain
|
|
445
446
|
for (const pattern of SMOL_MODEL_PRIORITY) {
|
|
447
|
+
// Try exact match with provider prefix
|
|
448
|
+
const providerMatch = availableModels.find((m) => `${m.provider}/${m.id}`.toLowerCase() === pattern);
|
|
449
|
+
if (providerMatch) return providerMatch;
|
|
450
|
+
|
|
446
451
|
// Try exact match first
|
|
447
|
-
const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern
|
|
452
|
+
const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern);
|
|
448
453
|
if (exactMatch) return exactMatch;
|
|
449
454
|
|
|
450
455
|
// Try fuzzy match (substring)
|
|
451
|
-
const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern
|
|
456
|
+
const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern));
|
|
452
457
|
if (fuzzyMatch) return fuzzyMatch;
|
|
453
458
|
}
|
|
454
459
|
|
package/src/core/sdk.ts
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
|
-
import type { Model } from "@mariozechner/pi-ai";
|
|
31
30
|
import { Agent, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
31
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
32
32
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
33
33
|
import chalk from "chalk";
|
|
34
34
|
// Import discovery to register all providers on startup
|
|
@@ -37,6 +37,7 @@ import { loadSync as loadCapability } from "../capability/index";
|
|
|
37
37
|
import { type Rule, ruleCapability } from "../capability/rule";
|
|
38
38
|
import { getAgentDir, getConfigDirPaths } from "../config";
|
|
39
39
|
import { initializeWithSettings } from "../discovery";
|
|
40
|
+
import { registerAsyncCleanup } from "../modes/cleanup";
|
|
40
41
|
import { AgentSession } from "./agent-session";
|
|
41
42
|
import { AuthStorage } from "./auth-storage";
|
|
42
43
|
import {
|
|
@@ -67,6 +68,8 @@ import { SessionManager } from "./session-manager";
|
|
|
67
68
|
import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager";
|
|
68
69
|
import { loadSkills as loadSkillsInternal, type Skill } from "./skills";
|
|
69
70
|
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands";
|
|
71
|
+
import { closeAllConnections } from "./ssh/connection-manager";
|
|
72
|
+
import { unmountAll } from "./ssh/sshfs-mount";
|
|
70
73
|
import {
|
|
71
74
|
buildSystemPrompt as buildSystemPromptInternal,
|
|
72
75
|
loadProjectContextFiles as loadContextFilesInternal,
|
|
@@ -83,6 +86,7 @@ import {
|
|
|
83
86
|
createGrepTool,
|
|
84
87
|
createLsTool,
|
|
85
88
|
createReadTool,
|
|
89
|
+
createSshTool,
|
|
86
90
|
createTools,
|
|
87
91
|
createWriteTool,
|
|
88
92
|
filterRulebookRules,
|
|
@@ -204,6 +208,7 @@ export {
|
|
|
204
208
|
// Individual tool factories (for custom usage)
|
|
205
209
|
createReadTool,
|
|
206
210
|
createBashTool,
|
|
211
|
+
createSshTool,
|
|
207
212
|
createEditTool,
|
|
208
213
|
createWriteTool,
|
|
209
214
|
createGrepTool,
|
|
@@ -399,6 +404,23 @@ function isCustomTool(tool: CustomTool | ToolDefinition): tool is CustomTool {
|
|
|
399
404
|
|
|
400
405
|
const TOOL_DEFINITION_MARKER = Symbol("__isToolDefinition");
|
|
401
406
|
|
|
407
|
+
let sshCleanupRegistered = false;
|
|
408
|
+
|
|
409
|
+
async function cleanupSshResources(): Promise<void> {
|
|
410
|
+
const results = await Promise.allSettled([closeAllConnections(), unmountAll()]);
|
|
411
|
+
for (const result of results) {
|
|
412
|
+
if (result.status === "rejected") {
|
|
413
|
+
logger.warn("SSH cleanup failed", { error: String(result.reason) });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function registerSshCleanup(): void {
|
|
419
|
+
if (sshCleanupRegistered) return;
|
|
420
|
+
sshCleanupRegistered = true;
|
|
421
|
+
registerAsyncCleanup(() => cleanupSshResources());
|
|
422
|
+
}
|
|
423
|
+
|
|
402
424
|
function customToolToDefinition(tool: CustomTool): ToolDefinition {
|
|
403
425
|
const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = {
|
|
404
426
|
name: tool.name,
|
|
@@ -471,7 +493,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
|
|
|
471
493
|
* const { session } = await createAgentSession();
|
|
472
494
|
*
|
|
473
495
|
* // With explicit model
|
|
474
|
-
* import { getModel } from '@
|
|
496
|
+
* import { getModel } from '@oh-my-pi/pi-ai';
|
|
475
497
|
* const { session } = await createAgentSession({
|
|
476
498
|
* model: getModel('anthropic', 'claude-opus-4-5'),
|
|
477
499
|
* thinkingLevel: 'high',
|
|
@@ -498,6 +520,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
498
520
|
const agentDir = options.agentDir ?? getDefaultAgentDir();
|
|
499
521
|
const eventBus = options.eventBus ?? createEventBus();
|
|
500
522
|
|
|
523
|
+
registerSshCleanup();
|
|
524
|
+
|
|
501
525
|
// Use provided or create AuthStorage and ModelRegistry
|
|
502
526
|
const authStorage = options.authStorage ?? (await discoverAuthStorage(agentDir));
|
|
503
527
|
const modelRegistry = options.modelRegistry ?? (await discoverModels(authStorage, agentDir));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, join, resolve } from "node:path";
|
|
2
|
-
import type { ImageContent, Message, TextContent, Usage } from "@mariozechner/pi-ai";
|
|
3
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { nanoid } from "nanoid";
|
|
5
5
|
import { getAgentDir as getDefaultAgentDir } from "../config";
|
|
6
6
|
import { resizeImage } from "../utils/image-resize";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createBranchSummaryMessage,
|
|
12
12
|
createCompactionSummaryMessage,
|
|
13
13
|
createCustomMessage,
|
|
14
|
+
type FileMentionMessage,
|
|
14
15
|
type HookMessage,
|
|
15
16
|
} from "./messages";
|
|
16
17
|
import type { SessionStorage, SessionStorageWriter } from "./session-storage";
|
|
@@ -1179,7 +1180,7 @@ export class SessionManager {
|
|
|
1179
1180
|
* so it is easier to find them.
|
|
1180
1181
|
* These need to be appended via appendCompaction() and appendBranchSummary() methods.
|
|
1181
1182
|
*/
|
|
1182
|
-
appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage): string {
|
|
1183
|
+
appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage | FileMentionMessage): string {
|
|
1183
1184
|
const entry: SessionMessageEntry = {
|
|
1184
1185
|
type: "message",
|
|
1185
1186
|
id: generateId(this.byId),
|
|
@@ -179,6 +179,8 @@ export interface Settings {
|
|
|
179
179
|
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
|
|
180
180
|
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
|
|
181
181
|
doubleEscapeAction?: "branch" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
|
182
|
+
/** Environment variables to set automatically on startup */
|
|
183
|
+
env?: Record<string, string>;
|
|
182
184
|
extensions?: string[]; // Array of extension file paths
|
|
183
185
|
skills?: SkillsSettings;
|
|
184
186
|
commands?: CommandsSettings;
|
|
@@ -379,6 +381,29 @@ export class SettingsManager {
|
|
|
379
381
|
this.globalSettings = initialSettings;
|
|
380
382
|
const projectSettings = this.loadProjectSettings();
|
|
381
383
|
this.settings = normalizeSettings(deepMergeSettings(this.globalSettings, projectSettings));
|
|
384
|
+
|
|
385
|
+
// Apply environment variables from settings
|
|
386
|
+
this.applyEnvironmentVariables();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Apply environment variables from settings to process.env
|
|
391
|
+
* Only sets variables that are not already set in the environment
|
|
392
|
+
*/
|
|
393
|
+
applyEnvironmentVariables(): void {
|
|
394
|
+
const envVars = this.settings.env;
|
|
395
|
+
if (!envVars || typeof envVars !== "object") {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
400
|
+
if (typeof key === "string" && typeof value === "string") {
|
|
401
|
+
// Only set if not already present in environment (allow override with env vars)
|
|
402
|
+
if (!(key in process.env)) {
|
|
403
|
+
process.env[key] = value;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
382
407
|
}
|
|
383
408
|
|
|
384
409
|
/** Create a SettingsManager that loads from files */
|
|
@@ -1169,4 +1194,49 @@ export class SettingsManager {
|
|
|
1169
1194
|
this.globalSettings.doubleEscapeAction = action;
|
|
1170
1195
|
this.save();
|
|
1171
1196
|
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Get environment variables from settings
|
|
1200
|
+
*/
|
|
1201
|
+
getEnvironmentVariables(): Record<string, string> {
|
|
1202
|
+
return { ...(this.settings.env ?? {}) };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Set environment variables in settings (not process.env)
|
|
1207
|
+
* This will be applied on next startup or reload
|
|
1208
|
+
*/
|
|
1209
|
+
setEnvironmentVariables(envVars: Record<string, string>): void {
|
|
1210
|
+
this.globalSettings.env = { ...envVars };
|
|
1211
|
+
this.save();
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Clear all environment variables from settings
|
|
1216
|
+
*/
|
|
1217
|
+
clearEnvironmentVariables(): void {
|
|
1218
|
+
delete this.globalSettings.env;
|
|
1219
|
+
this.save();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Set a single environment variable in settings
|
|
1224
|
+
*/
|
|
1225
|
+
setEnvironmentVariable(key: string, value: string): void {
|
|
1226
|
+
if (!this.globalSettings.env) {
|
|
1227
|
+
this.globalSettings.env = {};
|
|
1228
|
+
}
|
|
1229
|
+
this.globalSettings.env[key] = value;
|
|
1230
|
+
this.save();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Remove a single environment variable from settings
|
|
1235
|
+
*/
|
|
1236
|
+
removeEnvironmentVariable(key: string): void {
|
|
1237
|
+
if (this.globalSettings.env) {
|
|
1238
|
+
delete this.globalSettings.env[key];
|
|
1239
|
+
this.save();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1172
1242
|
}
|