@oh-my-pi/pi-coding-agent 4.0.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +2 -1
- package/docs/sdk.md +0 -3
- package/package.json +6 -5
- package/src/config.ts +9 -0
- package/src/core/agent-storage.ts +450 -0
- package/src/core/auth-storage.ts +111 -184
- package/src/core/compaction/branch-summarization.ts +5 -4
- package/src/core/compaction/compaction.ts +7 -6
- package/src/core/compaction/utils.ts +6 -11
- package/src/core/custom-commands/bundled/review/index.ts +22 -94
- package/src/core/custom-share.ts +66 -0
- package/src/core/history-storage.ts +174 -0
- package/src/core/index.ts +1 -0
- package/src/core/keybindings.ts +3 -0
- package/src/core/prompt-templates.ts +271 -1
- package/src/core/sdk.ts +14 -3
- package/src/core/settings-manager.ts +100 -34
- package/src/core/slash-commands.ts +4 -1
- package/src/core/storage-migration.ts +215 -0
- package/src/core/system-prompt.ts +87 -289
- package/src/core/title-generator.ts +3 -2
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +2 -1
- package/src/core/tools/calculator.ts +2 -1
- package/src/core/tools/edit.ts +2 -1
- package/src/core/tools/find.ts +2 -1
- package/src/core/tools/gemini-image.ts +2 -1
- package/src/core/tools/git.ts +2 -2
- package/src/core/tools/grep.ts +2 -1
- package/src/core/tools/index.test.ts +0 -28
- package/src/core/tools/index.ts +0 -6
- package/src/core/tools/lsp/index.ts +2 -1
- package/src/core/tools/output.ts +2 -1
- package/src/core/tools/read.ts +4 -1
- package/src/core/tools/ssh.ts +4 -2
- package/src/core/tools/task/agents.ts +56 -30
- package/src/core/tools/task/commands.ts +9 -8
- package/src/core/tools/task/index.ts +7 -15
- package/src/core/tools/web-fetch.ts +2 -1
- package/src/core/tools/web-search/auth.ts +106 -16
- package/src/core/tools/web-search/index.ts +3 -2
- package/src/core/tools/web-search/providers/anthropic.ts +44 -6
- package/src/core/tools/write.ts +2 -1
- package/src/core/voice.ts +3 -1
- package/src/main.ts +1 -1
- package/src/migrations.ts +20 -20
- package/src/modes/interactive/components/custom-editor.ts +7 -0
- package/src/modes/interactive/components/history-search.ts +158 -0
- package/src/modes/interactive/controllers/command-controller.ts +527 -0
- package/src/modes/interactive/controllers/event-controller.ts +340 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
- package/src/modes/interactive/controllers/input-controller.ts +585 -0
- package/src/modes/interactive/controllers/selector-controller.ts +585 -0
- package/src/modes/interactive/interactive-mode.ts +370 -3115
- package/src/modes/interactive/theme/theme.ts +5 -5
- package/src/modes/interactive/types.ts +189 -0
- package/src/modes/interactive/utils/ui-helpers.ts +449 -0
- package/src/modes/interactive/utils/voice-manager.ts +96 -0
- package/src/prompts/{explore.md → agents/explore.md} +7 -5
- package/src/prompts/agents/frontmatter.md +7 -0
- package/src/prompts/{plan.md → agents/plan.md} +3 -3
- package/src/prompts/{task.md → agents/task.md} +1 -1
- package/src/prompts/review-request.md +44 -8
- package/src/prompts/system/custom-system-prompt.md +80 -0
- package/src/prompts/system/file-operations.md +12 -0
- package/src/prompts/system/system-prompt.md +232 -0
- package/src/prompts/system/title-system.md +2 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +9 -3
- package/src/core/tools/rulebook.ts +0 -132
- package/src/prompts/system-prompt.md +0 -43
- package/src/prompts/title-system.md +0 -8
- /package/src/prompts/{architect-plan.md → agents/architect-plan.md} +0 -0
- /package/src/prompts/{implement-with-critic.md → agents/implement-with-critic.md} +0 -0
- /package/src/prompts/{implement.md → agents/implement.md} +0 -0
- /package/src/prompts/{init.md → agents/init.md} +0 -0
- /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
- /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
- /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
- /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
- /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
- /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
- /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrates legacy JSON storage (settings.json, auth.json) to SQLite-based agent.db.
|
|
3
|
+
* Settings migrate only when the DB has no settings; auth merges per-provider when missing.
|
|
4
|
+
* Original JSON files are backed up to .bak and removed after successful migration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getAgentDbPath } from "../config";
|
|
8
|
+
import { AgentStorage } from "./agent-storage";
|
|
9
|
+
import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "./auth-storage";
|
|
10
|
+
import { logger } from "./logger";
|
|
11
|
+
import type { Settings } from "./settings-manager";
|
|
12
|
+
|
|
13
|
+
/** Paths configuration for the storage migration process. */
|
|
14
|
+
type MigrationPaths = {
|
|
15
|
+
/** Directory containing agent.db */
|
|
16
|
+
agentDir: string;
|
|
17
|
+
/** Path to legacy settings.json file */
|
|
18
|
+
settingsPath: string;
|
|
19
|
+
/** Candidate paths to search for auth.json (checked in order) */
|
|
20
|
+
authPaths: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Result of the JSON-to-SQLite storage migration. */
|
|
24
|
+
export interface StorageMigrationResult {
|
|
25
|
+
/** Whether settings.json was migrated to agent.db */
|
|
26
|
+
migratedSettings: boolean;
|
|
27
|
+
/** Whether auth.json was migrated to agent.db */
|
|
28
|
+
migratedAuth: boolean;
|
|
29
|
+
/** Non-fatal issues encountered during migration */
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Type guard for plain objects.
|
|
35
|
+
* @param value - Value to check
|
|
36
|
+
* @returns True if value is a non-null, non-array object
|
|
37
|
+
*/
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Transforms legacy settings to current schema (e.g., queueMode -> steeringMode).
|
|
44
|
+
* @param settings - Settings object potentially containing deprecated keys
|
|
45
|
+
* @returns Settings with deprecated keys renamed to current equivalents
|
|
46
|
+
*/
|
|
47
|
+
function migrateLegacySettings(settings: Settings): Settings {
|
|
48
|
+
const migrated = { ...settings } as Record<string, unknown>;
|
|
49
|
+
if ("queueMode" in migrated && !("steeringMode" in migrated)) {
|
|
50
|
+
migrated.steeringMode = migrated.queueMode;
|
|
51
|
+
delete migrated.queueMode;
|
|
52
|
+
}
|
|
53
|
+
return migrated as Settings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalizes credential entries to array format (legacy stored single credentials).
|
|
58
|
+
* @param entry - Single credential or array of credentials
|
|
59
|
+
* @returns Array of credentials (empty if entry is undefined)
|
|
60
|
+
*/
|
|
61
|
+
function normalizeCredentialEntry(entry: AuthCredentialEntry | undefined): AuthCredential[] {
|
|
62
|
+
if (!entry) return [];
|
|
63
|
+
return Array.isArray(entry) ? entry : [entry];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reads and parses a JSON file.
|
|
68
|
+
* @param path - Path to the JSON file
|
|
69
|
+
* @returns Parsed JSON content, or null if file doesn't exist or parsing fails
|
|
70
|
+
*/
|
|
71
|
+
async function readJsonFile<T>(path: string): Promise<T | null> {
|
|
72
|
+
try {
|
|
73
|
+
const file = Bun.file(path);
|
|
74
|
+
if (!(await file.exists())) return null;
|
|
75
|
+
const content = await file.text();
|
|
76
|
+
return JSON.parse(content) as T;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.warn("Storage migration failed to read JSON", { path, error: String(error) });
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Backs up a JSON file to .bak and removes the original.
|
|
85
|
+
* Prevents re-migration on subsequent runs.
|
|
86
|
+
* @param path - Path to the JSON file to backup
|
|
87
|
+
*/
|
|
88
|
+
async function backupJson(path: string): Promise<void> {
|
|
89
|
+
const file = Bun.file(path);
|
|
90
|
+
if (!(await file.exists())) return;
|
|
91
|
+
|
|
92
|
+
const backupPath = `${path}.bak`;
|
|
93
|
+
try {
|
|
94
|
+
const content = await file.arrayBuffer();
|
|
95
|
+
await Bun.write(backupPath, content);
|
|
96
|
+
await file.unlink();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
logger.warn("Storage migration failed to backup JSON", { path, error: String(error) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Migrates settings.json to SQLite storage if DB is empty.
|
|
104
|
+
* @param storage - AgentStorage instance to migrate into
|
|
105
|
+
* @param settingsPath - Path to legacy settings.json
|
|
106
|
+
* @param warnings - Array to collect non-fatal warnings
|
|
107
|
+
* @returns True if migration was performed
|
|
108
|
+
*/
|
|
109
|
+
async function migrateSettings(storage: AgentStorage, settingsPath: string, warnings: string[]): Promise<boolean> {
|
|
110
|
+
const settingsFile = Bun.file(settingsPath);
|
|
111
|
+
const settingsExists = await settingsFile.exists();
|
|
112
|
+
const hasDbSettings = storage.getSettings() !== null;
|
|
113
|
+
|
|
114
|
+
if (!settingsExists) return false;
|
|
115
|
+
if (hasDbSettings) {
|
|
116
|
+
warnings.push(`settings.json exists but agent.db is authoritative: ${settingsPath}`);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const settingsJson = await readJsonFile<Settings>(settingsPath);
|
|
121
|
+
if (!settingsJson) return false;
|
|
122
|
+
|
|
123
|
+
storage.saveSettings(migrateLegacySettings(settingsJson));
|
|
124
|
+
await backupJson(settingsPath);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Finds the first valid auth.json from candidate paths (checked in priority order).
|
|
130
|
+
* @param authPaths - Candidate paths to search (e.g., project-local before global)
|
|
131
|
+
* @returns First valid auth file with its path and parsed data, or null if none found
|
|
132
|
+
*/
|
|
133
|
+
async function findFirstAuthJson(authPaths: string[]): Promise<{ path: string; data: AuthStorageData } | null> {
|
|
134
|
+
for (const authPath of authPaths) {
|
|
135
|
+
const data = await readJsonFile<AuthStorageData>(authPath);
|
|
136
|
+
if (data && isRecord(data)) {
|
|
137
|
+
return { path: authPath, data };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validates that a credential has a recognized type.
|
|
145
|
+
* @param entry - Credential to validate
|
|
146
|
+
* @returns True if credential type is api_key or oauth
|
|
147
|
+
*/
|
|
148
|
+
function isValidCredential(entry: AuthCredential): boolean {
|
|
149
|
+
return entry.type === "api_key" || entry.type === "oauth";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Migrates auth.json to SQLite storage for providers missing in agent.db.
|
|
154
|
+
* @param storage - AgentStorage instance to migrate into
|
|
155
|
+
* @param authPaths - Candidate paths to search for auth.json
|
|
156
|
+
* @param warnings - Array to collect non-fatal warnings
|
|
157
|
+
* @returns True if migration was performed
|
|
158
|
+
*/
|
|
159
|
+
async function migrateAuth(storage: AgentStorage, authPaths: string[], warnings: string[]): Promise<boolean> {
|
|
160
|
+
const authJson = await findFirstAuthJson(authPaths);
|
|
161
|
+
if (!authJson) return false;
|
|
162
|
+
|
|
163
|
+
let sawValid = false;
|
|
164
|
+
let migratedAny = false;
|
|
165
|
+
|
|
166
|
+
for (const [provider, entry] of Object.entries(authJson.data)) {
|
|
167
|
+
const credentials = normalizeCredentialEntry(entry)
|
|
168
|
+
.filter(isValidCredential)
|
|
169
|
+
.map((credential) => credential);
|
|
170
|
+
|
|
171
|
+
if (credentials.length === 0) continue;
|
|
172
|
+
sawValid = true;
|
|
173
|
+
|
|
174
|
+
if (storage.listAuthCredentials(provider).length > 0) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
storage.replaceAuthCredentialsForProvider(provider, credentials);
|
|
179
|
+
migratedAny = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (sawValid) {
|
|
183
|
+
await backupJson(authJson.path);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!migratedAny && sawValid) {
|
|
187
|
+
warnings.push(`auth.json entries already present in agent.db: ${authJson.path}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return migratedAny;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Migrates legacy JSON files (settings.json, auth.json) to SQLite-based agent.db.
|
|
195
|
+
* Settings migrate only when the DB has no settings; auth merges per-provider when missing.
|
|
196
|
+
* @param paths - Configuration specifying locations of legacy files and target DB
|
|
197
|
+
* @returns Result indicating what was migrated and any warnings encountered
|
|
198
|
+
*/
|
|
199
|
+
export async function migrateJsonStorage(paths: MigrationPaths): Promise<StorageMigrationResult> {
|
|
200
|
+
const storage = AgentStorage.open(getAgentDbPath(paths.agentDir));
|
|
201
|
+
const warnings: string[] = [];
|
|
202
|
+
|
|
203
|
+
const [migratedSettings, migratedAuth] = await Promise.all([
|
|
204
|
+
migrateSettings(storage, paths.settingsPath, warnings),
|
|
205
|
+
migrateAuth(storage, paths.authPaths, warnings),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
if (warnings.length > 0) {
|
|
209
|
+
for (const warning of warnings) {
|
|
210
|
+
logger.warn("Storage migration warning", { warning });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { migratedSettings, migratedAuth, warnings };
|
|
215
|
+
}
|
|
@@ -7,14 +7,14 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { contextFileCapability } from "../capability/context-file";
|
|
10
|
-
import type { Rule } from "../capability/rule";
|
|
11
10
|
import { systemPromptCapability } from "../capability/system-prompt";
|
|
12
11
|
import { type ContextFile, loadSync, type SystemPrompt as SystemPromptFile } from "../discovery/index";
|
|
13
|
-
import
|
|
12
|
+
import customSystemPromptTemplate from "../prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
13
|
+
import systemPromptTemplate from "../prompts/system/system-prompt.md" with { type: "text" };
|
|
14
|
+
import { renderPromptTemplate } from "./prompt-templates";
|
|
14
15
|
import type { SkillsSettings } from "./settings-manager";
|
|
15
|
-
import {
|
|
16
|
+
import { loadSkills, type Skill } from "./skills";
|
|
16
17
|
import type { ToolName } from "./tools/index";
|
|
17
|
-
import { formatRulesForPrompt } from "./tools/rulebook";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Execute a git command synchronously and return stdout or null on failure.
|
|
@@ -25,11 +25,19 @@ function execGit(args: string[], cwd: string): string | null {
|
|
|
25
25
|
return result.stdout.toString().trim() || null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
interface GitContext {
|
|
29
|
+
isRepo: boolean;
|
|
30
|
+
currentBranch: string;
|
|
31
|
+
mainBranch: string;
|
|
32
|
+
status: string;
|
|
33
|
+
commits: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
/**
|
|
29
37
|
* Load git context for the system prompt.
|
|
30
|
-
* Returns
|
|
38
|
+
* Returns structured git data or null if not in a git repo.
|
|
31
39
|
*/
|
|
32
|
-
export function loadGitContext(cwd: string):
|
|
40
|
+
export function loadGitContext(cwd: string): GitContext | null {
|
|
33
41
|
// Check if inside a git repo
|
|
34
42
|
const isGitRepo = execGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
35
43
|
if (isGitRepo !== "true") return null;
|
|
@@ -48,22 +56,19 @@ export function loadGitContext(cwd: string): string | null {
|
|
|
48
56
|
|
|
49
57
|
// Get git status (porcelain format for parsing)
|
|
50
58
|
const gitStatus = execGit(["status", "--porcelain"], cwd);
|
|
51
|
-
const
|
|
59
|
+
const status = gitStatus?.trim() || "(clean)";
|
|
52
60
|
|
|
53
61
|
// Get recent commits
|
|
54
62
|
const recentCommits = execGit(["log", "--oneline", "-5"], cwd);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
return `This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.
|
|
58
|
-
Current branch: ${currentBranch}
|
|
59
|
-
|
|
60
|
-
Main branch (you will usually use this for PRs): ${mainBranch}
|
|
61
|
-
|
|
62
|
-
Status:
|
|
63
|
-
${statusText}
|
|
63
|
+
const commits = recentCommits?.trim() || "(no commits)";
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
return {
|
|
66
|
+
isRepo: true,
|
|
67
|
+
currentBranch,
|
|
68
|
+
mainBranch,
|
|
69
|
+
status,
|
|
70
|
+
commits,
|
|
71
|
+
};
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
/** Tool descriptions for system prompt */
|
|
@@ -88,47 +93,6 @@ const toolDescriptions: Record<ToolName, string> = {
|
|
|
88
93
|
report_finding: "Report a finding during code review",
|
|
89
94
|
};
|
|
90
95
|
|
|
91
|
-
function applyTemplate(template: string, values: Record<string, string>): string {
|
|
92
|
-
let output = template;
|
|
93
|
-
for (const [key, value] of Object.entries(values)) {
|
|
94
|
-
output = output.replaceAll(`{{${key}}}`, value);
|
|
95
|
-
}
|
|
96
|
-
return output;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function appendBlock(prompt: string, block: string | null | undefined, separator = "\n\n"): string {
|
|
100
|
-
if (!block) return prompt;
|
|
101
|
-
if (block.startsWith("\n")) {
|
|
102
|
-
return `${prompt}${block}`;
|
|
103
|
-
}
|
|
104
|
-
return `${prompt}${separator}${block}`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function appendSection(prompt: string, title: string, content: string | null | undefined): string {
|
|
108
|
-
if (!content) return prompt;
|
|
109
|
-
return `${prompt}\n\n# ${title}\n\n${content}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function formatProjectContext(contextFiles: Array<{ path: string; content: string; depth?: number }>): string | null {
|
|
113
|
-
if (contextFiles.length === 0) return null;
|
|
114
|
-
const parts: string[] = ["The following project context files have been loaded:", ""];
|
|
115
|
-
for (const { path: filePath, content } of contextFiles) {
|
|
116
|
-
parts.push(`## ${filePath}`, "", content, "");
|
|
117
|
-
}
|
|
118
|
-
return parts.join("\n").trimEnd();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function formatToolDescriptions(tools: Map<string, { description: string; label: string }> | undefined): string | null {
|
|
122
|
-
if (!tools || tools.size === 0) return null;
|
|
123
|
-
return Array.from(tools.entries())
|
|
124
|
-
.map(([name, { description }]) => `- ${name}: ${description}`)
|
|
125
|
-
.join("\n");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function buildPromptFooter(dateTime: string, cwd: string): string {
|
|
129
|
-
return `Current date and time: ${dateTime}\nCurrent working directory: ${cwd}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
96
|
function execCommand(args: string[]): string | null {
|
|
133
97
|
const result = Bun.spawnSync(args, { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
134
98
|
if (result.exitCode !== 0) return null;
|
|
@@ -504,7 +468,7 @@ function getDiskInfo(): string | null {
|
|
|
504
468
|
}
|
|
505
469
|
}
|
|
506
470
|
|
|
507
|
-
function
|
|
471
|
+
function getEnvironmentInfo(): Array<{ label: string; value: string }> {
|
|
508
472
|
// Load cached system info or collect fresh
|
|
509
473
|
let sysInfo = loadSystemInfoCache();
|
|
510
474
|
if (!sysInfo) {
|
|
@@ -512,127 +476,19 @@ function formatEnvironmentInfo(): string {
|
|
|
512
476
|
saveSystemInfoCache(sysInfo);
|
|
513
477
|
}
|
|
514
478
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
["WM", getWindowManager()],
|
|
479
|
+
return [
|
|
480
|
+
{ label: "OS", value: sysInfo.os },
|
|
481
|
+
{ label: "Distro", value: sysInfo.distro },
|
|
482
|
+
{ label: "Kernel", value: sysInfo.kernel },
|
|
483
|
+
{ label: "Arch", value: sysInfo.arch },
|
|
484
|
+
{ label: "CPU", value: sysInfo.cpu },
|
|
485
|
+
{ label: "GPU", value: sysInfo.gpu },
|
|
486
|
+
{ label: "Disk", value: sysInfo.disk },
|
|
487
|
+
{ label: "Shell", value: getShellName() },
|
|
488
|
+
{ label: "Terminal", value: getTerminalName() },
|
|
489
|
+
{ label: "DE", value: getDesktopEnvironment() },
|
|
490
|
+
{ label: "WM", value: getWindowManager() },
|
|
528
491
|
];
|
|
529
|
-
return items.map(([label, value]) => `- ${label}: ${value}`).join("\n");
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Generate anti-bash rules section if the agent has both bash and specialized tools.
|
|
534
|
-
* Only include rules for tools that are actually available.
|
|
535
|
-
*/
|
|
536
|
-
function generateAntiBashRules(tools: ToolName[]): string | null {
|
|
537
|
-
const hasBash = tools.includes("bash");
|
|
538
|
-
if (!hasBash) return null;
|
|
539
|
-
|
|
540
|
-
const hasRead = tools.includes("read");
|
|
541
|
-
const hasGrep = tools.includes("grep");
|
|
542
|
-
const hasFind = tools.includes("find");
|
|
543
|
-
const hasLs = tools.includes("ls");
|
|
544
|
-
const hasEdit = tools.includes("edit");
|
|
545
|
-
const hasLsp = tools.includes("lsp");
|
|
546
|
-
const hasGit = tools.includes("git");
|
|
547
|
-
|
|
548
|
-
// Only show rules if we have specialized tools that should be preferred
|
|
549
|
-
const hasSpecializedTools = hasRead || hasGrep || hasFind || hasLs || hasEdit || hasGit;
|
|
550
|
-
if (!hasSpecializedTools) return null;
|
|
551
|
-
|
|
552
|
-
const lines: string[] = [];
|
|
553
|
-
lines.push("## Tool Usage Rules — MANDATORY\n");
|
|
554
|
-
lines.push("### Forbidden Bash Patterns");
|
|
555
|
-
lines.push("NEVER use bash for these operations:\n");
|
|
556
|
-
|
|
557
|
-
if (hasRead) lines.push("- **File reading**: Use `read` instead of cat/head/tail/less/more");
|
|
558
|
-
if (hasGrep) lines.push("- **Content search**: Use `grep` instead of grep/rg/ag/ack");
|
|
559
|
-
if (hasFind) lines.push("- **File finding**: Use `find` instead of find/fd/locate");
|
|
560
|
-
if (hasLs) lines.push("- **Directory listing**: Use `ls` instead of bash ls");
|
|
561
|
-
if (hasEdit) lines.push("- **File editing**: Use `edit` instead of sed/awk/perl -pi/echo >/cat <<EOF");
|
|
562
|
-
if (hasGit) lines.push("- **Git operations**: Use `git` tool instead of bash git commands");
|
|
563
|
-
|
|
564
|
-
lines.push("\n### Tool Preference (highest → lowest priority)");
|
|
565
|
-
const ladder: string[] = [];
|
|
566
|
-
if (hasLsp) ladder.push("lsp (go-to-definition, references, type info) — DETERMINISTIC");
|
|
567
|
-
if (hasGrep) ladder.push("grep (text/regex search)");
|
|
568
|
-
if (hasFind) ladder.push("find (locate files by pattern)");
|
|
569
|
-
if (hasRead) ladder.push("read (view file contents)");
|
|
570
|
-
if (hasEdit) ladder.push("edit (precise text replacement)");
|
|
571
|
-
if (hasGit) ladder.push("git (structured git operations with safety guards)");
|
|
572
|
-
ladder.push(`bash (ONLY for ${hasGit ? "" : "git, "}npm, docker, make, cargo, etc.)`);
|
|
573
|
-
lines.push(ladder.map((t, i) => `${i + 1}. ${t}`).join("\n"));
|
|
574
|
-
|
|
575
|
-
// Add LSP guidance if available
|
|
576
|
-
if (hasLsp) {
|
|
577
|
-
lines.push("\n### LSP — Preferred for Semantic Queries");
|
|
578
|
-
lines.push("Use `lsp` instead of grep/bash when you need:");
|
|
579
|
-
lines.push("- **Where is X defined?** → `lsp definition`");
|
|
580
|
-
lines.push("- **What calls X?** → `lsp incoming_calls`");
|
|
581
|
-
lines.push("- **What does X call?** → `lsp outgoing_calls`");
|
|
582
|
-
lines.push("- **What type is X?** → `lsp hover`");
|
|
583
|
-
lines.push("- **What symbols are in this file?** → `lsp symbols`");
|
|
584
|
-
lines.push("- **Find symbol across codebase** → `lsp workspace_symbols`\n");
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Add Git guidance if available
|
|
588
|
-
if (hasGit) {
|
|
589
|
-
lines.push("\n### Git Tool — Preferred for Git Operations");
|
|
590
|
-
lines.push("Use `git` instead of bash git when you need:");
|
|
591
|
-
lines.push(
|
|
592
|
-
"- **Status/diff/log**: `git { operation: 'status' }`, `git { operation: 'diff' }`, `git { operation: 'log' }`",
|
|
593
|
-
);
|
|
594
|
-
lines.push(
|
|
595
|
-
"- **Commit workflow**: `git { operation: 'add', paths: [...] }` then `git { operation: 'commit', message: '...' }`",
|
|
596
|
-
);
|
|
597
|
-
lines.push("- **Branching**: `git { operation: 'branch', action: 'create', name: '...' }`");
|
|
598
|
-
lines.push("- **GitHub PRs**: `git { operation: 'pr', action: 'create', title: '...', body: '...' }`");
|
|
599
|
-
lines.push(
|
|
600
|
-
"- **GitHub Issues**: `git { operation: 'issue', action: 'list' }` or `{ operation: 'issue', number: 123 }`",
|
|
601
|
-
);
|
|
602
|
-
lines.push(
|
|
603
|
-
"The git tool provides typed output, safety guards, and a clean API for all git and GitHub operations.\n",
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Add SSH remote filesystem guidance if available
|
|
608
|
-
const hasSSH = tools.includes("ssh");
|
|
609
|
-
if (hasSSH) {
|
|
610
|
-
lines.push("\n### SSH Command Execution");
|
|
611
|
-
lines.push(
|
|
612
|
-
"**Critical**: Each SSH host runs a specific shell. **You MUST match commands to the host's shell type**.",
|
|
613
|
-
);
|
|
614
|
-
lines.push("Check the host list in the ssh tool description. Shell types:");
|
|
615
|
-
lines.push("- linux/bash, linux/zsh, macos/bash, macos/zsh: ls, cat, grep, find, ps, df, uname");
|
|
616
|
-
lines.push("- windows/bash, windows/sh: ls, cat, grep, find (Windows with WSL/Cygwin — Unix commands)");
|
|
617
|
-
lines.push("- windows/cmd: dir, type, findstr, tasklist, systeminfo");
|
|
618
|
-
lines.push("- windows/powershell: Get-ChildItem, Get-Content, Select-String, Get-Process");
|
|
619
|
-
lines.push("");
|
|
620
|
-
lines.push("### SSH Filesystems");
|
|
621
|
-
lines.push("Mounted at `~/.omp/remote/<hostname>/` — use read/edit/write tools directly.");
|
|
622
|
-
lines.push("Windows paths need colon: `~/.omp/remote/host/C:/Users/...` not `C/Users/...`\n");
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Add search-first protocol
|
|
626
|
-
if (hasGrep || hasFind) {
|
|
627
|
-
lines.push("\n### Search-First Protocol");
|
|
628
|
-
lines.push("Before reading any file:");
|
|
629
|
-
if (hasFind) lines.push("1. Unknown structure → `find` to see file layout");
|
|
630
|
-
if (hasGrep) lines.push("2. Known location → `grep` for specific symbol/error");
|
|
631
|
-
if (hasRead) lines.push("3. Use `read offset/limit` for line ranges, not entire large files");
|
|
632
|
-
lines.push("4. Never read a large file hoping to find something — search first");
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return lines.join("\n");
|
|
636
492
|
}
|
|
637
493
|
|
|
638
494
|
/** Resolve input as file path or literal string */
|
|
@@ -732,7 +588,7 @@ export interface BuildSystemPromptOptions {
|
|
|
732
588
|
/** Pre-loaded skills (skips discovery if provided). */
|
|
733
589
|
skills?: Skill[];
|
|
734
590
|
/** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
|
|
735
|
-
rules?:
|
|
591
|
+
rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
|
|
736
592
|
}
|
|
737
593
|
|
|
738
594
|
/** Build the system prompt with tools, guidelines, and context */
|
|
@@ -746,7 +602,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
746
602
|
cwd,
|
|
747
603
|
contextFiles: providedContextFiles,
|
|
748
604
|
skills: providedSkills,
|
|
749
|
-
rules
|
|
605
|
+
rules,
|
|
750
606
|
} = options;
|
|
751
607
|
const resolvedCwd = cwd ?? process.cwd();
|
|
752
608
|
const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
|
|
@@ -770,121 +626,63 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
770
626
|
// Resolve context files: use provided or discover
|
|
771
627
|
const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
|
|
772
628
|
|
|
773
|
-
// Build
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
629
|
+
// Build tool descriptions array
|
|
630
|
+
// Priority: toolNames (explicit list) > tools (Map) > defaults
|
|
631
|
+
const defaultToolNames: ToolName[] = ["read", "bash", "edit", "write"];
|
|
632
|
+
let toolNamesArray: string[];
|
|
633
|
+
if (toolNames !== undefined) {
|
|
634
|
+
// Explicit toolNames list provided (could be empty)
|
|
635
|
+
toolNamesArray = toolNames;
|
|
636
|
+
} else if (tools !== undefined) {
|
|
637
|
+
// Tools map provided
|
|
638
|
+
toolNamesArray = Array.from(tools.keys());
|
|
639
|
+
} else {
|
|
640
|
+
// Use defaults
|
|
641
|
+
toolNamesArray = defaultToolNames;
|
|
642
|
+
}
|
|
643
|
+
const toolDescriptionsArray = toolNamesArray.map((name) => ({
|
|
644
|
+
name,
|
|
645
|
+
description: toolDescriptions[name as ToolName] ?? "",
|
|
646
|
+
}));
|
|
779
647
|
|
|
780
648
|
// Resolve skills: use provided or discover
|
|
781
649
|
const skills =
|
|
782
650
|
providedSkills ??
|
|
783
651
|
(skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).skills : []);
|
|
784
652
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
? `${systemPromptCustomization}\n\n${resolvedCustomPrompt}`
|
|
788
|
-
: resolvedCustomPrompt;
|
|
653
|
+
// Get git context
|
|
654
|
+
const git = loadGitContext(resolvedCwd);
|
|
789
655
|
|
|
790
|
-
|
|
791
|
-
prompt = appendSection(prompt, "Project Context", formatProjectContext(contextFiles));
|
|
792
|
-
prompt = appendSection(prompt, "Tools", formatToolDescriptions(tools));
|
|
793
|
-
|
|
794
|
-
const gitContext = loadGitContext(resolvedCwd);
|
|
795
|
-
prompt = appendSection(prompt, "Git Status", gitContext);
|
|
796
|
-
|
|
797
|
-
if (tools?.has("read") && skills.length > 0) {
|
|
798
|
-
prompt = appendBlock(prompt, formatSkillsForPrompt(skills));
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (rulebookRules && rulebookRules.length > 0) {
|
|
802
|
-
prompt = appendBlock(prompt, formatRulesForPrompt(rulebookRules));
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
prompt = appendBlock(prompt, buildPromptFooter(dateTime, resolvedCwd), "\n");
|
|
806
|
-
|
|
807
|
-
return prompt;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Generate anti-bash rules (returns null if not applicable)
|
|
811
|
-
const antiBashSection = generateAntiBashRules(Array.from(tools?.keys() ?? []));
|
|
812
|
-
const environmentInfo = formatEnvironmentInfo();
|
|
813
|
-
|
|
814
|
-
// Build guidelines based on which tools are actually available
|
|
815
|
-
const guidelinesList: string[] = [];
|
|
816
|
-
|
|
817
|
-
const hasBash = tools?.has("bash");
|
|
818
|
-
const hasEdit = tools?.has("edit");
|
|
819
|
-
const hasWrite = tools?.has("write");
|
|
656
|
+
// Filter skills to only include those with read tool
|
|
820
657
|
const hasRead = tools?.has("read");
|
|
658
|
+
const filteredSkills = hasRead ? skills : [];
|
|
821
659
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
"When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did",
|
|
850
|
-
);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Always include these
|
|
854
|
-
guidelinesList.push("Be concise in your responses");
|
|
855
|
-
guidelinesList.push("Show file paths clearly when working with files");
|
|
856
|
-
|
|
857
|
-
const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
|
|
858
|
-
|
|
859
|
-
// Build the prompt with anti-bash rules prominently placed
|
|
860
|
-
const antiBashBlock = antiBashSection ? `\n${antiBashSection}\n` : "";
|
|
861
|
-
let prompt = applyTemplate(systemPromptTemplate, {
|
|
862
|
-
toolsList,
|
|
863
|
-
antiBashSection: antiBashBlock,
|
|
864
|
-
guidelines,
|
|
865
|
-
environmentInfo,
|
|
660
|
+
if (resolvedCustomPrompt) {
|
|
661
|
+
return renderPromptTemplate(customSystemPromptTemplate, {
|
|
662
|
+
systemPromptCustomization: systemPromptCustomization ?? "",
|
|
663
|
+
customPrompt: resolvedCustomPrompt,
|
|
664
|
+
appendPrompt: resolvedAppendPrompt ?? "",
|
|
665
|
+
contextFiles,
|
|
666
|
+
toolDescriptions: toolDescriptionsArray,
|
|
667
|
+
git,
|
|
668
|
+
skills: filteredSkills,
|
|
669
|
+
rules: rules ?? [],
|
|
670
|
+
dateTime,
|
|
671
|
+
cwd: resolvedCwd,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return renderPromptTemplate(systemPromptTemplate, {
|
|
676
|
+
tools: toolNamesArray,
|
|
677
|
+
toolDescriptions: toolDescriptionsArray,
|
|
678
|
+
environment: getEnvironmentInfo(),
|
|
679
|
+
systemPromptCustomization: systemPromptCustomization ?? "",
|
|
680
|
+
contextFiles,
|
|
681
|
+
git,
|
|
682
|
+
skills: filteredSkills,
|
|
683
|
+
rules: rules ?? [],
|
|
684
|
+
dateTime,
|
|
685
|
+
cwd: resolvedCwd,
|
|
686
|
+
appendSystemPrompt: resolvedAppendPrompt ?? "",
|
|
866
687
|
});
|
|
867
|
-
|
|
868
|
-
prompt = appendBlock(prompt, resolvedAppendPrompt);
|
|
869
|
-
prompt = appendSection(prompt, "Project Context", formatProjectContext(contextFiles));
|
|
870
|
-
|
|
871
|
-
const gitContext = loadGitContext(resolvedCwd);
|
|
872
|
-
prompt = appendSection(prompt, "Git Status", gitContext);
|
|
873
|
-
|
|
874
|
-
if (hasRead && skills.length > 0) {
|
|
875
|
-
prompt = appendBlock(prompt, formatSkillsForPrompt(skills));
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
if (rulebookRules && rulebookRules.length > 0) {
|
|
879
|
-
prompt = appendBlock(prompt, formatRulesForPrompt(rulebookRules));
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
prompt = appendBlock(prompt, buildPromptFooter(dateTime, resolvedCwd), "\n");
|
|
883
|
-
|
|
884
|
-
// Prepend SYSTEM.md customization if present
|
|
885
|
-
if (systemPromptCustomization) {
|
|
886
|
-
prompt = `${systemPromptCustomization}\n\n${prompt}`;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
return prompt;
|
|
890
688
|
}
|