@oh-my-pi/pi-coding-agent 4.3.0 → 4.3.1
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 +11 -0
- package/package.json +5 -5
- package/src/cli/update-cli.ts +2 -2
- package/src/config.ts +5 -5
- package/src/core/auth-storage.ts +6 -1
- package/src/core/custom-commands/loader.ts +3 -1
- package/src/core/custom-tools/loader.ts +1 -18
- package/src/core/extensions/loader.ts +5 -21
- package/src/core/hooks/loader.ts +1 -18
- package/src/core/keybindings.ts +3 -1
- package/src/core/logger.ts +1 -2
- package/src/core/prompt-templates.ts +5 -4
- package/src/core/sdk.ts +5 -3
- package/src/core/skills.ts +5 -4
- package/src/core/tools/exa/mcp-client.ts +2 -2
- package/src/core/tools/task/agents.ts +5 -64
- package/src/core/tools/task/commands.ts +7 -33
- package/src/core/tools/task/discovery.ts +4 -66
- package/src/core/tools/task/executor.ts +32 -3
- package/src/core/tools/task/index.ts +11 -2
- package/src/core/tools/task/render.ts +25 -15
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/task/worker-protocol.ts +2 -1
- package/src/core/tools/task/worker.ts +2 -1
- package/src/core/tools/web-scrapers/huggingface.ts +1 -1
- package/src/core/tools/web-scrapers/readthedocs.ts +1 -1
- package/src/core/tools/web-scrapers/types.ts +1 -1
- package/src/core/tools/web-search/auth.ts +5 -3
- package/src/discovery/codex.ts +3 -1
- package/src/discovery/helpers.ts +124 -3
- package/src/migrations.ts +11 -9
- package/src/modes/interactive/components/extensions/state-manager.ts +19 -18
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/agents/reviewer.md +32 -4
- package/src/prompts/tools/task.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [4.3.1] - 2026-01-11
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Expanded system prompt with defensive reasoning guidance and assumption checks
|
|
10
|
+
- Allowed agent frontmatter to override subagent thinking level, clamped to model capabilities
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Ensured reviewer agents use structured output schemas and include reported findings in task outputs
|
|
15
|
+
|
|
5
16
|
## [4.3.0] - 2026-01-11
|
|
6
17
|
|
|
7
18
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "4.3.
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "4.3.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "4.3.
|
|
45
|
-
"@oh-my-pi/pi-tui": "4.3.
|
|
42
|
+
"@oh-my-pi/pi-ai": "4.3.1",
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "4.3.1",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "4.3.1",
|
|
45
|
+
"@oh-my-pi/pi-tui": "4.3.1",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -144,8 +144,8 @@ async function updateViaBun(): Promise<void> {
|
|
|
144
144
|
try {
|
|
145
145
|
execSync(`bun update -g ${PACKAGE}`, { stdio: "inherit" });
|
|
146
146
|
console.log(chalk.green(`\n${theme.status.success} Update complete`));
|
|
147
|
-
} catch {
|
|
148
|
-
throw new Error("bun update failed");
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new Error("bun update failed", { cause: error });
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
|
|
5
4
|
// Embed package.json at build time for config
|
|
6
5
|
import packageJson from "../package.json" with { type: "json" };
|
|
6
|
+
import { logger } from "./core/logger";
|
|
7
7
|
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// App Config (from embedded package.json)
|
|
@@ -244,8 +244,8 @@ export function readConfigFile<T = unknown>(
|
|
|
244
244
|
content: JSON.parse(content) as T,
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
|
-
} catch {
|
|
248
|
-
|
|
247
|
+
} catch (error) {
|
|
248
|
+
logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -275,8 +275,8 @@ export function readAllConfigFiles<T = unknown>(
|
|
|
275
275
|
content: JSON.parse(content) as T,
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
|
-
} catch {
|
|
279
|
-
|
|
278
|
+
} catch (error) {
|
|
279
|
+
logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
package/src/core/auth-storage.ts
CHANGED
|
@@ -939,7 +939,12 @@ export class AuthStorage {
|
|
|
939
939
|
|
|
940
940
|
this.recordSessionCredential(provider, sessionId, "oauth", selection.index);
|
|
941
941
|
return result.apiKey;
|
|
942
|
-
} catch {
|
|
942
|
+
} catch (error) {
|
|
943
|
+
logger.warn("OAuth token refresh failed, removing credential", {
|
|
944
|
+
provider,
|
|
945
|
+
index: selection.index,
|
|
946
|
+
error: String(error),
|
|
947
|
+
});
|
|
943
948
|
this.removeCredentialAt(provider, selection.index);
|
|
944
949
|
if (this.getCredentialsForProvider(provider).some((credential) => credential.type === "oauth")) {
|
|
945
950
|
return this.getApiKey(provider, sessionId, options);
|
|
@@ -11,6 +11,7 @@ import * as typebox from "@sinclair/typebox";
|
|
|
11
11
|
import { getAgentDir, getConfigDirs } from "../../config";
|
|
12
12
|
import * as piCodingAgent from "../../index";
|
|
13
13
|
import { execCommand } from "../exec";
|
|
14
|
+
import { logger } from "../logger";
|
|
14
15
|
import { createReviewCommand } from "./bundled/review";
|
|
15
16
|
import { createWorktreeCommand } from "./bundled/wt";
|
|
16
17
|
import type {
|
|
@@ -110,7 +111,8 @@ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions =
|
|
|
110
111
|
let entries: Dirent[];
|
|
111
112
|
try {
|
|
112
113
|
entries = readdirSync(commandsDir, { withFileTypes: true });
|
|
113
|
-
} catch {
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logger.warn("Failed to read custom commands directory", { path: commandsDir, error: String(error) });
|
|
114
116
|
continue;
|
|
115
117
|
}
|
|
116
118
|
for (const entry of entries) {
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* to avoid import resolution issues with custom tools loaded from user directories.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import * as os from "node:os";
|
|
9
8
|
import * as path from "node:path";
|
|
10
9
|
import * as typebox from "@sinclair/typebox";
|
|
11
10
|
import { toolCapability } from "../../capability/tool";
|
|
12
11
|
import { type CustomTool, loadCapability } from "../../discovery";
|
|
12
|
+
import { expandPath } from "../../discovery/helpers";
|
|
13
13
|
import * as piCodingAgent from "../../index";
|
|
14
14
|
import { theme } from "../../modes/interactive/theme/theme";
|
|
15
15
|
import type { ExecOptions } from "../exec";
|
|
@@ -19,23 +19,6 @@ import { logger } from "../logger";
|
|
|
19
19
|
import { getAllPluginToolPaths } from "../plugins/loader";
|
|
20
20
|
import type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from "./types";
|
|
21
21
|
|
|
22
|
-
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
23
|
-
|
|
24
|
-
function normalizeUnicodeSpaces(str: string): string {
|
|
25
|
-
return str.replace(UNICODE_SPACES, " ");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function expandPath(p: string): string {
|
|
29
|
-
const normalized = normalizeUnicodeSpaces(p);
|
|
30
|
-
if (normalized.startsWith("~/")) {
|
|
31
|
-
return path.join(os.homedir(), normalized.slice(2));
|
|
32
|
-
}
|
|
33
|
-
if (normalized.startsWith("~")) {
|
|
34
|
-
return path.join(os.homedir(), normalized.slice(1));
|
|
35
|
-
}
|
|
36
|
-
return normalized;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
22
|
/**
|
|
40
23
|
* Resolve tool path.
|
|
41
24
|
* - Absolute paths used as-is
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
6
|
import * as path from "node:path";
|
|
8
7
|
import type { KeyId } from "@oh-my-pi/pi-tui";
|
|
9
8
|
import * as TypeBox from "@sinclair/typebox";
|
|
10
9
|
import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
|
|
11
10
|
import { loadCapability } from "../../discovery";
|
|
12
|
-
import { getExtensionNameFromPath } from "../../discovery/helpers";
|
|
11
|
+
import { expandPath, getExtensionNameFromPath } from "../../discovery/helpers";
|
|
13
12
|
import * as piCodingAgent from "../../index";
|
|
14
13
|
import { createEventBus, type EventBus } from "../event-bus";
|
|
15
14
|
import type { ExecOptions } from "../exec";
|
|
@@ -27,23 +26,6 @@ import type {
|
|
|
27
26
|
ToolDefinition,
|
|
28
27
|
} from "./types";
|
|
29
28
|
|
|
30
|
-
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
31
|
-
|
|
32
|
-
function normalizeUnicodeSpaces(str: string): string {
|
|
33
|
-
return str.replace(UNICODE_SPACES, " ");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function expandPath(p: string): string {
|
|
37
|
-
const normalized = normalizeUnicodeSpaces(p);
|
|
38
|
-
if (normalized.startsWith("~/")) {
|
|
39
|
-
return path.join(homedir(), normalized.slice(2));
|
|
40
|
-
}
|
|
41
|
-
if (normalized.startsWith("~")) {
|
|
42
|
-
return path.join(homedir(), normalized.slice(1));
|
|
43
|
-
}
|
|
44
|
-
return normalized;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
29
|
function resolvePath(extPath: string, cwd: string): string {
|
|
48
30
|
const expanded = expandPath(extPath);
|
|
49
31
|
if (path.isAbsolute(expanded)) {
|
|
@@ -291,7 +273,8 @@ function readExtensionManifest(packageJsonPath: string): ExtensionManifest | nul
|
|
|
291
273
|
return manifest;
|
|
292
274
|
}
|
|
293
275
|
return null;
|
|
294
|
-
} catch {
|
|
276
|
+
} catch (error) {
|
|
277
|
+
logger.warn("Failed to read extension manifest", { path: packageJsonPath, error: String(error) });
|
|
295
278
|
return null;
|
|
296
279
|
}
|
|
297
280
|
}
|
|
@@ -370,7 +353,8 @@ function discoverExtensionsInDir(dir: string): string[] {
|
|
|
370
353
|
}
|
|
371
354
|
}
|
|
372
355
|
}
|
|
373
|
-
} catch {
|
|
356
|
+
} catch (error) {
|
|
357
|
+
logger.warn("Failed to discover extensions in directory", { path: dir, error: String(error) });
|
|
374
358
|
return [];
|
|
375
359
|
}
|
|
376
360
|
|
package/src/core/hooks/loader.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Hook loader - loads TypeScript hook modules using native Bun import.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import * as os from "node:os";
|
|
6
5
|
import * as path from "node:path";
|
|
7
6
|
import * as typebox from "@sinclair/typebox";
|
|
8
7
|
import { hookCapability } from "../../capability/hook";
|
|
9
8
|
import type { Hook } from "../../discovery";
|
|
10
9
|
import { loadCapability } from "../../discovery";
|
|
10
|
+
import { expandPath } from "../../discovery/helpers";
|
|
11
11
|
import * as piCodingAgent from "../../index";
|
|
12
12
|
import { logger } from "../logger";
|
|
13
13
|
import type { HookMessage } from "../messages";
|
|
@@ -84,23 +84,6 @@ export interface LoadHooksResult {
|
|
|
84
84
|
errors: Array<{ path: string; error: string }>;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
88
|
-
|
|
89
|
-
function normalizeUnicodeSpaces(str: string): string {
|
|
90
|
-
return str.replace(UNICODE_SPACES, " ");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function expandPath(p: string): string {
|
|
94
|
-
const normalized = normalizeUnicodeSpaces(p);
|
|
95
|
-
if (normalized.startsWith("~/")) {
|
|
96
|
-
return path.join(os.homedir(), normalized.slice(2));
|
|
97
|
-
}
|
|
98
|
-
if (normalized.startsWith("~")) {
|
|
99
|
-
return path.join(os.homedir(), normalized.slice(1));
|
|
100
|
-
}
|
|
101
|
-
return normalized;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
87
|
/**
|
|
105
88
|
* Resolve hook path.
|
|
106
89
|
* - Absolute paths used as-is
|
package/src/core/keybindings.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
setEditorKeybindings,
|
|
11
11
|
} from "@oh-my-pi/pi-tui";
|
|
12
12
|
import { getAgentDir } from "../config";
|
|
13
|
+
import { logger } from "./logger";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Application-level actions (coding agent specific).
|
|
@@ -136,7 +137,8 @@ export class KeybindingsManager {
|
|
|
136
137
|
if (!existsSync(path)) return {};
|
|
137
138
|
try {
|
|
138
139
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
139
|
-
} catch {
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger.warn("Failed to parse keybindings config", { path, error: String(error) });
|
|
140
142
|
return {};
|
|
141
143
|
}
|
|
142
144
|
}
|
package/src/core/logger.ts
CHANGED
|
@@ -10,11 +10,10 @@ import { homedir } from "node:os";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import winston from "winston";
|
|
12
12
|
import DailyRotateFile from "winston-daily-rotate-file";
|
|
13
|
-
import { CONFIG_DIR_NAME } from "../config";
|
|
14
13
|
|
|
15
14
|
/** Get the logs directory (~/.omp/logs/) */
|
|
16
15
|
function getLogsDir(): string {
|
|
17
|
-
return join(homedir(),
|
|
16
|
+
return join(homedir(), ".omp", "logs");
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/** Ensure logs directory exists */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import Handlebars from "handlebars";
|
|
3
3
|
import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
|
|
4
|
+
import { logger } from "./logger";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Represents a prompt template loaded from a markdown file
|
|
@@ -448,12 +449,12 @@ async function loadTemplatesFromDir(
|
|
|
448
449
|
source: sourceStr,
|
|
449
450
|
});
|
|
450
451
|
}
|
|
451
|
-
} catch (
|
|
452
|
-
|
|
452
|
+
} catch (error) {
|
|
453
|
+
logger.warn("Failed to load prompt template", { path: fullPath, error: String(error) });
|
|
453
454
|
}
|
|
454
455
|
}
|
|
455
|
-
} catch (
|
|
456
|
-
|
|
456
|
+
} catch (error) {
|
|
457
|
+
logger.warn("Failed to scan prompt templates directory", { dir, error: String(error) });
|
|
457
458
|
}
|
|
458
459
|
|
|
459
460
|
return templates;
|
package/src/core/sdk.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
31
|
-
import type
|
|
31
|
+
import { type Message, type Model, supportsXhigh } 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
|
|
@@ -631,6 +631,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
631
631
|
// Clamp to model capabilities
|
|
632
632
|
if (!model || !model.reasoning) {
|
|
633
633
|
thinkingLevel = "off";
|
|
634
|
+
} else if (thinkingLevel === "xhigh" && !supportsXhigh(model)) {
|
|
635
|
+
thinkingLevel = "high";
|
|
634
636
|
}
|
|
635
637
|
|
|
636
638
|
let skills: Skill[];
|
|
@@ -1021,8 +1023,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1021
1023
|
});
|
|
1022
1024
|
lspServers = result.servers;
|
|
1023
1025
|
time("warmupLspServers");
|
|
1024
|
-
} catch {
|
|
1025
|
-
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
logger.warn("LSP server warmup failed", { cwd, error: String(error) });
|
|
1026
1028
|
}
|
|
1027
1029
|
}
|
|
1028
1030
|
|
package/src/core/skills.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { SourceMeta } from "../capability/types";
|
|
|
7
7
|
import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
|
|
8
8
|
import { loadCapability } from "../discovery";
|
|
9
9
|
import { parseFrontmatter } from "../discovery/helpers";
|
|
10
|
+
import { logger } from "./logger";
|
|
10
11
|
import type { SkillsSettings } from "./settings-manager";
|
|
11
12
|
|
|
12
13
|
// Re-export SkillFrontmatter for backward compatibility
|
|
@@ -67,8 +68,8 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
|
|
|
67
68
|
source: options.source,
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
|
-
} catch {
|
|
71
|
-
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.warn("Failed to load skill", { path: skillFile, error: String(error) });
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -131,8 +132,8 @@ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
|
|
|
131
132
|
source: "custom",
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
|
-
} catch {
|
|
135
|
-
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.warn("Failed to load skill", { path: skillFile, error: String(error) });
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -292,8 +292,8 @@ export async function fetchMCPToolSchema(
|
|
|
292
292
|
mcpSchemaCache.set(cacheKey, tool);
|
|
293
293
|
return tool;
|
|
294
294
|
}
|
|
295
|
-
} catch {
|
|
296
|
-
|
|
295
|
+
} catch (error) {
|
|
296
|
+
logger.warn("Failed to fetch MCP tool schema", { mcpToolName, isWebsetsTool, error: String(error) });
|
|
297
297
|
}
|
|
298
298
|
return null;
|
|
299
299
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Agents are embedded at build time via Bun's import with { type: "text" }.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { parseAgentFields, parseFrontmatter } from "../../../discovery/helpers";
|
|
7
8
|
import exploreMd from "../../../prompts/agents/explore.md" with { type: "text" };
|
|
8
9
|
// Embed agent markdown files at build time
|
|
9
10
|
import agentFrontmatterTemplate from "../../../prompts/agents/frontmatter.md" with { type: "text" };
|
|
@@ -18,6 +19,7 @@ interface AgentFrontmatter {
|
|
|
18
19
|
description: string;
|
|
19
20
|
spawns?: string;
|
|
20
21
|
model?: string;
|
|
22
|
+
thinkingLevel?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
interface EmbeddedAgentDef {
|
|
@@ -71,80 +73,19 @@ const EMBEDDED_AGENTS: { name: string; content: string }[] = EMBEDDED_AGENT_DEFS
|
|
|
71
73
|
content: buildAgentContent(def),
|
|
72
74
|
}));
|
|
73
75
|
|
|
74
|
-
/**
|
|
75
|
-
* Parse YAML frontmatter from markdown content.
|
|
76
|
-
*/
|
|
77
|
-
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
78
|
-
const frontmatter: Record<string, string> = {};
|
|
79
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
80
|
-
|
|
81
|
-
if (!normalized.startsWith("---")) {
|
|
82
|
-
return { frontmatter, body: normalized };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
86
|
-
if (endIndex === -1) {
|
|
87
|
-
return { frontmatter, body: normalized };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
91
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
92
|
-
|
|
93
|
-
for (const line of frontmatterBlock.split("\n")) {
|
|
94
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
95
|
-
if (match) {
|
|
96
|
-
let value = match[2].trim();
|
|
97
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
98
|
-
value = value.slice(1, -1);
|
|
99
|
-
}
|
|
100
|
-
frontmatter[match[1]] = value;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { frontmatter, body };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
76
|
/**
|
|
108
77
|
* Parse an agent from embedded content.
|
|
109
78
|
*/
|
|
110
79
|
function parseAgent(fileName: string, content: string, source: AgentSource): AgentDefinition | null {
|
|
111
80
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
81
|
+
const fields = parseAgentFields(frontmatter);
|
|
112
82
|
|
|
113
|
-
if (!
|
|
83
|
+
if (!fields) {
|
|
114
84
|
return null;
|
|
115
85
|
}
|
|
116
86
|
|
|
117
|
-
const tools = frontmatter.tools
|
|
118
|
-
?.split(",")
|
|
119
|
-
.map((t) => t.trim())
|
|
120
|
-
.filter(Boolean);
|
|
121
|
-
|
|
122
|
-
// Parse spawns field
|
|
123
|
-
let spawns: string[] | "*" | undefined;
|
|
124
|
-
if (frontmatter.spawns !== undefined) {
|
|
125
|
-
const spawnsRaw = frontmatter.spawns.trim();
|
|
126
|
-
if (spawnsRaw === "*") {
|
|
127
|
-
spawns = "*";
|
|
128
|
-
} else if (spawnsRaw) {
|
|
129
|
-
spawns = spawnsRaw
|
|
130
|
-
.split(",")
|
|
131
|
-
.map((s) => s.trim())
|
|
132
|
-
.filter(Boolean);
|
|
133
|
-
if (spawns.length === 0) spawns = undefined;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Backward compat: infer spawns: "*" when tools includes "task"
|
|
138
|
-
if (spawns === undefined && tools?.includes("task")) {
|
|
139
|
-
spawns = "*";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
87
|
return {
|
|
143
|
-
|
|
144
|
-
description: frontmatter.description,
|
|
145
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
146
|
-
spawns,
|
|
147
|
-
model: frontmatter.model,
|
|
88
|
+
...fields,
|
|
148
89
|
systemPrompt: body,
|
|
149
90
|
source,
|
|
150
91
|
filePath: `embedded:${fileName}`,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { type SlashCommand, slashCommandCapability } from "../../../capability/slash-command";
|
|
9
9
|
import { loadCapability } from "../../../discovery";
|
|
10
|
+
import { parseFrontmatter } from "../../../discovery/helpers";
|
|
10
11
|
|
|
11
12
|
// Embed command markdown files at build time
|
|
12
13
|
import initMd from "../../../prompts/agents/init.md" with { type: "text" };
|
|
@@ -27,37 +28,10 @@ export interface WorkflowCommand {
|
|
|
27
28
|
filePath: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const frontmatter: Record<string, string> = {};
|
|
35
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
36
|
-
|
|
37
|
-
if (!normalized.startsWith("---")) {
|
|
38
|
-
return { frontmatter, body: normalized };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
42
|
-
if (endIndex === -1) {
|
|
43
|
-
return { frontmatter, body: normalized };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
47
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
48
|
-
|
|
49
|
-
for (const line of frontmatterBlock.split("\n")) {
|
|
50
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
51
|
-
if (match) {
|
|
52
|
-
let value = match[2].trim();
|
|
53
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
54
|
-
value = value.slice(1, -1);
|
|
55
|
-
}
|
|
56
|
-
frontmatter[match[1]] = value;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return { frontmatter, body };
|
|
31
|
+
/** Extract string value from frontmatter field */
|
|
32
|
+
function getString(frontmatter: Record<string, unknown>, key: string): string {
|
|
33
|
+
const value = frontmatter[key];
|
|
34
|
+
return typeof value === "string" ? value : "";
|
|
61
35
|
}
|
|
62
36
|
|
|
63
37
|
/** Cache for bundled commands */
|
|
@@ -79,7 +53,7 @@ export function loadBundledCommands(): WorkflowCommand[] {
|
|
|
79
53
|
|
|
80
54
|
commands.push({
|
|
81
55
|
name: cmdName,
|
|
82
|
-
description: frontmatter
|
|
56
|
+
description: getString(frontmatter, "description"),
|
|
83
57
|
instructions: body,
|
|
84
58
|
source: "bundled",
|
|
85
59
|
filePath: `embedded:${name}`,
|
|
@@ -115,7 +89,7 @@ export async function discoverCommands(cwd: string): Promise<WorkflowCommand[]>
|
|
|
115
89
|
|
|
116
90
|
commands.push({
|
|
117
91
|
name: cmd.name,
|
|
118
|
-
description: frontmatter
|
|
92
|
+
description: getString(frontmatter, "description"),
|
|
119
93
|
instructions: body,
|
|
120
94
|
source,
|
|
121
95
|
filePath: cmd.path,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { findAllNearestProjectConfigDirs, getConfigDirs } from "../../../config";
|
|
18
|
+
import { parseAgentFields, parseFrontmatter } from "../../../discovery/helpers";
|
|
18
19
|
import { loadBundledAgents } from "./agents";
|
|
19
20
|
import type { AgentDefinition, AgentSource } from "./types";
|
|
20
21
|
|
|
@@ -24,40 +25,6 @@ export interface DiscoveryResult {
|
|
|
24
25
|
projectAgentsDir: string | null;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
* Parse YAML frontmatter from markdown content.
|
|
29
|
-
*/
|
|
30
|
-
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
31
|
-
const frontmatter: Record<string, string> = {};
|
|
32
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
33
|
-
|
|
34
|
-
if (!normalized.startsWith("---")) {
|
|
35
|
-
return { frontmatter, body: normalized };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
39
|
-
if (endIndex === -1) {
|
|
40
|
-
return { frontmatter, body: normalized };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
44
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
45
|
-
|
|
46
|
-
for (const line of frontmatterBlock.split("\n")) {
|
|
47
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
48
|
-
if (match) {
|
|
49
|
-
let value = match[2].trim();
|
|
50
|
-
// Strip quotes
|
|
51
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
52
|
-
value = value.slice(1, -1);
|
|
53
|
-
}
|
|
54
|
-
frontmatter[match[1]] = value;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return { frontmatter, body };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
28
|
/**
|
|
62
29
|
* Load agents from a directory.
|
|
63
30
|
*/
|
|
@@ -95,43 +62,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
|
|
|
95
62
|
}
|
|
96
63
|
|
|
97
64
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
65
|
+
const fields = parseAgentFields(frontmatter);
|
|
98
66
|
|
|
99
|
-
|
|
100
|
-
if (!frontmatter.name || !frontmatter.description) {
|
|
67
|
+
if (!fields) {
|
|
101
68
|
continue;
|
|
102
69
|
}
|
|
103
70
|
|
|
104
|
-
const tools = frontmatter.tools
|
|
105
|
-
?.split(",")
|
|
106
|
-
.map((t) => t.trim())
|
|
107
|
-
.filter(Boolean);
|
|
108
|
-
|
|
109
|
-
// Parse spawns field
|
|
110
|
-
let spawns: string[] | "*" | undefined;
|
|
111
|
-
if (frontmatter.spawns !== undefined) {
|
|
112
|
-
const spawnsRaw = frontmatter.spawns.trim();
|
|
113
|
-
if (spawnsRaw === "*") {
|
|
114
|
-
spawns = "*";
|
|
115
|
-
} else if (spawnsRaw) {
|
|
116
|
-
spawns = spawnsRaw
|
|
117
|
-
.split(",")
|
|
118
|
-
.map((s) => s.trim())
|
|
119
|
-
.filter(Boolean);
|
|
120
|
-
if (spawns.length === 0) spawns = undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Backward compat: infer spawns: "*" when tools includes "task"
|
|
125
|
-
if (spawns === undefined && tools?.includes("task")) {
|
|
126
|
-
spawns = "*";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
71
|
agents.push({
|
|
130
|
-
|
|
131
|
-
description: frontmatter.description,
|
|
132
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
133
|
-
spawns,
|
|
134
|
-
model: frontmatter.model,
|
|
72
|
+
...fields,
|
|
135
73
|
systemPrompt: body,
|
|
136
74
|
source,
|
|
137
75
|
filePath,
|