@agentex/agent 0.0.24 → 0.0.26
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 +338 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/derived.d.ts +5 -3
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +11 -7
- package/dist/derived.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/acp/index.d.ts +1 -1
- package/dist/providers/acp/index.d.ts.map +1 -1
- package/dist/providers/acp/index.js +5 -97
- package/dist/providers/acp/index.js.map +1 -1
- package/dist/providers/acp/session.d.ts +8 -1
- package/dist/providers/acp/session.d.ts.map +1 -1
- package/dist/providers/acp/session.js +94 -0
- package/dist/providers/acp/session.js.map +1 -1
- package/dist/providers/claude/attach.d.ts +8 -0
- package/dist/providers/claude/attach.d.ts.map +1 -0
- package/dist/providers/claude/attach.js +113 -0
- package/dist/providers/claude/attach.js.map +1 -0
- package/dist/providers/claude/goal-capability.d.ts +15 -0
- package/dist/providers/claude/goal-capability.d.ts.map +1 -0
- package/dist/providers/claude/goal-capability.js +20 -0
- package/dist/providers/claude/goal-capability.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +8 -4
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +11 -9
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +29 -14
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +9 -0
- package/dist/providers/codex/attach.d.ts.map +1 -0
- package/dist/providers/codex/attach.js +93 -0
- package/dist/providers/codex/attach.js.map +1 -0
- package/dist/providers/codex/goal-capability.d.ts +13 -0
- package/dist/providers/codex/goal-capability.d.ts.map +1 -0
- package/dist/providers/codex/goal-capability.js +18 -0
- package/dist/providers/codex/goal-capability.js.map +1 -0
- package/dist/providers/codex/index.d.ts +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +9 -6
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/session.d.ts +11 -7
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +24 -12
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +28 -0
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
- package/dist/providers/codex/transcript-normalize.js +191 -0
- package/dist/providers/codex/transcript-normalize.js.map +1 -0
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +2 -2
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/openclaw/index.d.ts.map +1 -1
- package/dist/providers/openclaw/index.js +2 -2
- package/dist/providers/openclaw/index.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +3 -5
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/pi/index.d.ts.map +1 -1
- package/dist/providers/pi/index.js +3 -5
- package/dist/providers/pi/index.js.map +1 -1
- package/dist/providers/process/index.d.ts.map +1 -1
- package/dist/providers/process/index.js +2 -2
- package/dist/providers/process/index.js.map +1 -1
- package/dist/registry.d.ts +0 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +0 -4
- package/dist/registry.js.map +1 -1
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/record.d.ts +43 -0
- package/dist/sessions/record.d.ts.map +1 -0
- package/dist/sessions/record.js +85 -0
- package/dist/sessions/record.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/uuid.d.ts +7 -1
- package/dist/utils/uuid.d.ts.map +1 -1
- package/dist/utils/uuid.js +21 -1
- package/dist/utils/uuid.js.map +1 -1
- package/package.json +64 -7
- package/src/derived.ts +311 -0
- package/src/goals/controller.ts +442 -0
- package/src/goals/index.ts +21 -0
- package/src/goals/normalize.ts +173 -0
- package/src/goals/sentinel.ts +90 -0
- package/src/index.ts +270 -0
- package/src/providers/_shared/http-agent.ts +304 -0
- package/src/providers/acp/index.ts +103 -0
- package/src/providers/acp/parse.ts +131 -0
- package/src/providers/acp/session.ts +744 -0
- package/src/providers/claude/attach.ts +147 -0
- package/src/providers/claude/codec.ts +43 -0
- package/src/providers/claude/execute.ts +300 -0
- package/src/providers/claude/goal-capability.ts +21 -0
- package/src/providers/claude/index.ts +72 -0
- package/src/providers/claude/mcp.ts +82 -0
- package/src/providers/claude/parse.ts +824 -0
- package/src/providers/claude/session.ts +1192 -0
- package/src/providers/claude/transcript.ts +555 -0
- package/src/providers/codex/attach.ts +123 -0
- package/src/providers/codex/codec.ts +50 -0
- package/src/providers/codex/execute.ts +337 -0
- package/src/providers/codex/goal-capability.ts +19 -0
- package/src/providers/codex/index.ts +57 -0
- package/src/providers/codex/modes.ts +159 -0
- package/src/providers/codex/parse.ts +691 -0
- package/src/providers/codex/plan-mode.ts +49 -0
- package/src/providers/codex/session.ts +1287 -0
- package/src/providers/codex/transcript-normalize.ts +197 -0
- package/src/providers/codex/transcript.ts +487 -0
- package/src/providers/codex/usage-scanner.ts +178 -0
- package/src/providers/copilot/index.ts +19 -0
- package/src/providers/cursor/codec.ts +44 -0
- package/src/providers/cursor/execute.ts +271 -0
- package/src/providers/cursor/index.ts +25 -0
- package/src/providers/cursor/parse.ts +288 -0
- package/src/providers/gemini/index.ts +21 -0
- package/src/providers/openclaw/codec.ts +40 -0
- package/src/providers/openclaw/execute.ts +19 -0
- package/src/providers/openclaw/index.ts +29 -0
- package/src/providers/opencode/codec.ts +50 -0
- package/src/providers/opencode/event-parse.ts +141 -0
- package/src/providers/opencode/execute.ts +251 -0
- package/src/providers/opencode/http-session.ts +427 -0
- package/src/providers/opencode/index.ts +30 -0
- package/src/providers/opencode/parse.ts +203 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/pi/codec.ts +44 -0
- package/src/providers/pi/execute.ts +297 -0
- package/src/providers/pi/index.ts +30 -0
- package/src/providers/pi/parse.ts +231 -0
- package/src/providers/pi/session.ts +381 -0
- package/src/providers/process/execute.ts +148 -0
- package/src/providers/process/index.ts +52 -0
- package/src/registry.ts +40 -0
- package/src/sessions/index.ts +8 -0
- package/src/sessions/record.ts +108 -0
- package/src/types.ts +1638 -0
- package/src/utils/ask-user-question.ts +57 -0
- package/src/utils/auth.ts +661 -0
- package/src/utils/binary.ts +179 -0
- package/src/utils/endpoint.ts +172 -0
- package/src/utils/env.ts +63 -0
- package/src/utils/execute-all.ts +68 -0
- package/src/utils/exit-plan-mode.ts +40 -0
- package/src/utils/instructions.ts +427 -0
- package/src/utils/process.ts +223 -0
- package/src/utils/runtime-config.ts +100 -0
- package/src/utils/runtime-homes.ts +49 -0
- package/src/utils/skill-commands.ts +493 -0
- package/src/utils/skills.ts +500 -0
- package/src/utils/template.ts +16 -0
- package/src/utils/tool-names.ts +51 -0
- package/src/utils/uuid.ts +21 -0
- package/src/utils/workspace.ts +156 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import type { SkillRuntime } from "./skills.js";
|
|
5
|
+
import { getRuntimeHomeEnvVar, getDefaultRuntimeHome } from "./runtime-homes.js";
|
|
6
|
+
import { uuidv7 } from "./uuid.js";
|
|
7
|
+
|
|
8
|
+
export interface TempConfigResult {
|
|
9
|
+
/** Environment variables to pass to the child process (includes the
|
|
10
|
+
* runtime home env var pointing at the temp config dir). */
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
/** Path to the temporary config directory. */
|
|
13
|
+
configDir: string;
|
|
14
|
+
/** Remove the temporary config directory. */
|
|
15
|
+
cleanup(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TempConfigOptions {
|
|
19
|
+
/** Which runtime CLI tool to configure. */
|
|
20
|
+
runtime: SkillRuntime;
|
|
21
|
+
/** Base environment to extend. */
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
/** If true, copy the contents of the default home into the temp dir
|
|
24
|
+
* before applying overrides. Defaults to false. */
|
|
25
|
+
seedFromDefault?: boolean;
|
|
26
|
+
/** Files to write into the temp config dir (relative paths → content). */
|
|
27
|
+
overrides?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a temporary config directory for a CLI runtime, optionally seeded
|
|
32
|
+
* from the user's default home, with arbitrary file overrides applied.
|
|
33
|
+
*
|
|
34
|
+
* Returns an env object with the runtime home env var pointed at the temp dir,
|
|
35
|
+
* and a cleanup function to remove the temp dir when done.
|
|
36
|
+
*
|
|
37
|
+
* This is useful for running a CLI tool with custom configuration (e.g.
|
|
38
|
+
* injecting system prompts, custom settings) without modifying the user's
|
|
39
|
+
* real config directory.
|
|
40
|
+
*/
|
|
41
|
+
export async function withTempConfig(options: TempConfigOptions): Promise<TempConfigResult> {
|
|
42
|
+
const { runtime, seedFromDefault, overrides } = options;
|
|
43
|
+
|
|
44
|
+
const envVarName = getRuntimeHomeEnvVar(runtime);
|
|
45
|
+
const shortId = uuidv7().slice(0, 8);
|
|
46
|
+
const configDir = path.join(os.tmpdir(), `agentex-cfg-${runtime}-${shortId}`);
|
|
47
|
+
|
|
48
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
// Seed from the user's default home if requested
|
|
51
|
+
if (seedFromDefault) {
|
|
52
|
+
const defaultHome = getDefaultRuntimeHome(runtime);
|
|
53
|
+
try {
|
|
54
|
+
await copyDir(defaultHome, configDir);
|
|
55
|
+
} catch {
|
|
56
|
+
// Default home may not exist — non-fatal
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Apply overrides
|
|
61
|
+
if (overrides) {
|
|
62
|
+
for (const [relativePath, content] of Object.entries(overrides)) {
|
|
63
|
+
const target = path.join(configDir, relativePath);
|
|
64
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
65
|
+
await fs.writeFile(target, content, "utf-8");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Build env with the runtime home pointing to our temp dir
|
|
70
|
+
const env = { ...(options.env ?? {}) };
|
|
71
|
+
if (envVarName) {
|
|
72
|
+
env[envVarName] = configDir;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
env,
|
|
77
|
+
configDir,
|
|
78
|
+
async cleanup(): Promise<void> {
|
|
79
|
+
await fs.rm(configDir, { recursive: true, force: true });
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Internal helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
async function copyDir(src: string, dest: string): Promise<void> {
|
|
89
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const srcPath = path.join(src, entry.name);
|
|
92
|
+
const destPath = path.join(dest, entry.name);
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
await fs.mkdir(destPath, { recursive: true });
|
|
95
|
+
await copyDir(srcPath, destPath);
|
|
96
|
+
} else {
|
|
97
|
+
await fs.copyFile(srcPath, destPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import type { SkillRuntime } from "./skills.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Env var that overrides the global home directory for each runtime CLI.
|
|
7
|
+
* Returns null for runtimes that don't support home directory override.
|
|
8
|
+
*/
|
|
9
|
+
const ENV_VAR_MAP: Record<SkillRuntime, string | null> = {
|
|
10
|
+
claude: "CLAUDE_CONFIG_DIR",
|
|
11
|
+
codex: "CODEX_HOME",
|
|
12
|
+
gemini: "GEMINI_CONFIG_DIR",
|
|
13
|
+
cursor: "CURSOR_CONFIG_DIR",
|
|
14
|
+
opencode: "XDG_CONFIG_HOME", // affects ~/.config/opencode/
|
|
15
|
+
pi: "PI_HOME",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Home directory subpath (relative to the home base) for each runtime.
|
|
20
|
+
*/
|
|
21
|
+
const HOME_SUBPATH_MAP: Record<SkillRuntime, string[]> = {
|
|
22
|
+
claude: [".claude"],
|
|
23
|
+
codex: [".codex"],
|
|
24
|
+
gemini: [".gemini"],
|
|
25
|
+
cursor: [".cursor"],
|
|
26
|
+
opencode: [".config", "opencode"],
|
|
27
|
+
pi: [".pi"],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the environment variable name that overrides the global home
|
|
32
|
+
* directory for the given runtime CLI tool.
|
|
33
|
+
*
|
|
34
|
+
* Returns null for runtimes that don't support home directory override.
|
|
35
|
+
*/
|
|
36
|
+
export function getRuntimeHomeEnvVar(runtime: SkillRuntime): string | null {
|
|
37
|
+
return ENV_VAR_MAP[runtime] ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns the default global home directory path for the given runtime.
|
|
42
|
+
*
|
|
43
|
+
* Pass `homeDir` to resolve against a base other than the user's real home
|
|
44
|
+
* directory (useful for sandboxed installs and tests). Defaults to os.homedir().
|
|
45
|
+
*/
|
|
46
|
+
export function getDefaultRuntimeHome(runtime: SkillRuntime, homeDir: string = os.homedir()): string {
|
|
47
|
+
const subpath = HOME_SUBPATH_MAP[runtime] ?? [`.${runtime}`];
|
|
48
|
+
return path.join(homeDir, ...subpath);
|
|
49
|
+
}
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AgentSession, StreamEvent, TurnResult } from "../types.js";
|
|
4
|
+
import { listInstalledSkills } from "./skills.js";
|
|
5
|
+
|
|
6
|
+
export type SkillCommandSource = "configured" | "installed-user" | "installed-workspace" | "app";
|
|
7
|
+
|
|
8
|
+
export type SkillCommandExecution =
|
|
9
|
+
| {
|
|
10
|
+
kind: "provider-slash";
|
|
11
|
+
provider: string;
|
|
12
|
+
commandText: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
kind: "expanded-prompt";
|
|
16
|
+
provider: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface SkillCommandDescriptor {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
/** Frontmatter description. Discovery falls back to the first non-empty body paragraph. */
|
|
23
|
+
description?: string;
|
|
24
|
+
argumentHint?: string;
|
|
25
|
+
source: SkillCommandSource;
|
|
26
|
+
/** Skill directory for discovered commands. Expanded-prompt invocation also accepts a direct SKILL.md path. */
|
|
27
|
+
sourcePath?: string;
|
|
28
|
+
userInvocable: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether this command should be offered for invocation in the current host/session.
|
|
31
|
+
*
|
|
32
|
+
* Discovery defaults this to true. `reconcileSkillCommands` narrows Claude
|
|
33
|
+
* provider-slash commands against the runtime inventory, while Codex,
|
|
34
|
+
* expanded-prompt, and app commands pass through unchanged.
|
|
35
|
+
*/
|
|
36
|
+
available: boolean;
|
|
37
|
+
execution: SkillCommandExecution;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RuntimeCommandInventory {
|
|
41
|
+
provider: "claude" | "codex" | string;
|
|
42
|
+
sessionId?: string | null;
|
|
43
|
+
slashCommands: string[];
|
|
44
|
+
skills: string[];
|
|
45
|
+
source: "provider-init" | "configured" | "none";
|
|
46
|
+
raw?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SkillCommandDiagnostic {
|
|
50
|
+
level: "warning" | "error";
|
|
51
|
+
path?: string;
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DiscoverSkillCommandsOptions {
|
|
56
|
+
cwd?: string;
|
|
57
|
+
skillDirs?: string[];
|
|
58
|
+
includeInstalled?: false | "workspace" | "user" | "all";
|
|
59
|
+
runtime?: "claude" | "codex" | string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DiscoverSkillCommandsResult {
|
|
63
|
+
commands: SkillCommandDescriptor[];
|
|
64
|
+
diagnostics: SkillCommandDiagnostic[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ReconcileSkillCommandsOptions {
|
|
68
|
+
discovered: SkillCommandDescriptor[];
|
|
69
|
+
inventory?: RuntimeCommandInventory | null;
|
|
70
|
+
provider: "claude" | "codex" | string;
|
|
71
|
+
appCommands?: SkillCommandDescriptor[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface InvokeSkillOptions {
|
|
75
|
+
args?: string;
|
|
76
|
+
userRequest?: string;
|
|
77
|
+
provider?: "claude" | "codex" | string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ParsedSkillFile = {
|
|
81
|
+
body: string;
|
|
82
|
+
frontmatter: Record<string, string>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type DiscoveredSkillSource = Exclude<SkillCommandSource, "app">;
|
|
86
|
+
|
|
87
|
+
type CollectedSkillDir = {
|
|
88
|
+
dir: string;
|
|
89
|
+
source: DiscoveredSkillSource;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export function commandInventoryFromEvent(event: StreamEvent): RuntimeCommandInventory | null {
|
|
93
|
+
if (event.type !== "system" || event.subtype !== "init") return null;
|
|
94
|
+
|
|
95
|
+
const slashCommands = event.slashCommands ?? readStringArray(event.raw, "slash_commands") ?? [];
|
|
96
|
+
const skills = event.skills ?? readStringArray(event.raw, "skills") ?? [];
|
|
97
|
+
if (slashCommands.length === 0 && skills.length === 0) return null;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
provider: event.providerType,
|
|
101
|
+
sessionId: event.sessionId,
|
|
102
|
+
slashCommands,
|
|
103
|
+
skills,
|
|
104
|
+
source: "provider-init",
|
|
105
|
+
raw: event.raw,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function discoverSkillCommands(
|
|
110
|
+
options: DiscoverSkillCommandsOptions,
|
|
111
|
+
): Promise<DiscoverSkillCommandsResult> {
|
|
112
|
+
const diagnostics: SkillCommandDiagnostic[] = [];
|
|
113
|
+
const runtime = options.runtime ?? "claude";
|
|
114
|
+
const skillDirs = await collectSkillDirs(options, diagnostics);
|
|
115
|
+
const commands: SkillCommandDescriptor[] = [];
|
|
116
|
+
const seenNames = new Set<string>();
|
|
117
|
+
const seenRealpaths = new Set<string>();
|
|
118
|
+
|
|
119
|
+
for (const { dir, source } of skillDirs) {
|
|
120
|
+
const resolvedDir = path.resolve(dir);
|
|
121
|
+
const realDir = await realpathOrResolved(resolvedDir);
|
|
122
|
+
if (seenRealpaths.has(realDir)) {
|
|
123
|
+
diagnostics.push({
|
|
124
|
+
level: "warning",
|
|
125
|
+
path: resolvedDir,
|
|
126
|
+
message: "Duplicate skill path skipped",
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
seenRealpaths.add(realDir);
|
|
131
|
+
|
|
132
|
+
const command = await commandFromSkillDir(resolvedDir, source, runtime, diagnostics);
|
|
133
|
+
if (!command) continue;
|
|
134
|
+
if (seenNames.has(command.name)) {
|
|
135
|
+
diagnostics.push({
|
|
136
|
+
level: "warning",
|
|
137
|
+
path: resolvedDir,
|
|
138
|
+
message: `Duplicate skill command "${command.name}" skipped`,
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
seenNames.add(command.name);
|
|
143
|
+
commands.push(command);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { commands, diagnostics };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Merge discovered commands with host app commands and apply provider runtime availability.
|
|
151
|
+
*
|
|
152
|
+
* Today only Claude provider-slash commands are narrowed by runtime inventory.
|
|
153
|
+
* Commands for other providers or execution kinds keep their existing
|
|
154
|
+
* `available` value because AgentEx has no authoritative runtime inventory
|
|
155
|
+
* for them yet.
|
|
156
|
+
*/
|
|
157
|
+
export function reconcileSkillCommands(options: ReconcileSkillCommandsOptions): SkillCommandDescriptor[] {
|
|
158
|
+
const commands = [...options.discovered, ...(options.appCommands ?? [])];
|
|
159
|
+
|
|
160
|
+
if (options.provider !== "claude" || !options.inventory) {
|
|
161
|
+
return commands;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const availableNames = new Set([
|
|
165
|
+
...options.inventory.skills,
|
|
166
|
+
...options.inventory.slashCommands,
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
return commands.map((command) => {
|
|
170
|
+
if (command.execution.kind !== "provider-slash" || command.execution.provider !== "claude") {
|
|
171
|
+
return command;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
...command,
|
|
175
|
+
available: inventoryCandidatesFor(command).some((name) => availableNames.has(name)),
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function formatSlashInvocation(command: Pick<SkillCommandDescriptor, "name">, args?: string): string {
|
|
181
|
+
const trimmedArgs = args?.trim() ?? "";
|
|
182
|
+
return trimmedArgs ? `/${command.name} ${trimmedArgs}` : `/${command.name}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function invokeSkill(
|
|
186
|
+
session: AgentSession,
|
|
187
|
+
command: SkillCommandDescriptor,
|
|
188
|
+
options: InvokeSkillOptions = {},
|
|
189
|
+
): Promise<TurnResult> {
|
|
190
|
+
if (!command.available) {
|
|
191
|
+
throw new Error(`Skill command "${command.name}" is not available`);
|
|
192
|
+
}
|
|
193
|
+
if (options.provider && options.provider !== command.execution.provider) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Skill command "${command.name}" is configured for provider "${command.execution.provider}", not "${options.provider}"`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (command.execution.kind === "provider-slash") {
|
|
200
|
+
const handle = await session.send(formatCommandText(command.execution.commandText, options.args));
|
|
201
|
+
return handle.result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const prompt = await buildExpandedSkillPrompt(command, {
|
|
205
|
+
args: options.args,
|
|
206
|
+
userRequest: options.userRequest,
|
|
207
|
+
});
|
|
208
|
+
const handle = await session.send(prompt);
|
|
209
|
+
return handle.result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function buildExpandedSkillPrompt(
|
|
213
|
+
command: SkillCommandDescriptor,
|
|
214
|
+
options?: { args?: string; userRequest?: string },
|
|
215
|
+
): Promise<string> {
|
|
216
|
+
const skill = command.sourcePath
|
|
217
|
+
? await readSkill(command.sourcePath)
|
|
218
|
+
: { body: "", skillDir: undefined };
|
|
219
|
+
const args = options?.args?.trim() ?? "";
|
|
220
|
+
const userRequest = options?.userRequest?.trim() ?? "";
|
|
221
|
+
const substitutedBody = substituteSkillArguments(skill.body, args, skill.skillDir);
|
|
222
|
+
|
|
223
|
+
const parts = [
|
|
224
|
+
"Use the following skill for this request.",
|
|
225
|
+
"",
|
|
226
|
+
`Skill: ${command.name}`,
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
if (command.description) {
|
|
230
|
+
parts.push(`Description: ${command.description}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
parts.push("", "Skill instructions:", substitutedBody || "(No skill body was available.)");
|
|
234
|
+
|
|
235
|
+
if (args) {
|
|
236
|
+
parts.push("", "Arguments:", args);
|
|
237
|
+
}
|
|
238
|
+
if (userRequest) {
|
|
239
|
+
parts.push("", "User request:", userRequest);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return parts.join("\n");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function collectSkillDirs(
|
|
246
|
+
options: DiscoverSkillCommandsOptions,
|
|
247
|
+
diagnostics: SkillCommandDiagnostic[],
|
|
248
|
+
): Promise<CollectedSkillDir[]> {
|
|
249
|
+
const dirs = new Map<string, CollectedSkillDir>();
|
|
250
|
+
for (const dir of options.skillDirs ?? []) {
|
|
251
|
+
const resolved = path.resolve(dir);
|
|
252
|
+
dirs.set(resolved, { dir: resolved, source: "configured" });
|
|
253
|
+
}
|
|
254
|
+
if (!options.includeInstalled) return [...dirs.values()];
|
|
255
|
+
|
|
256
|
+
const addInstalled = async (
|
|
257
|
+
location: "global" | "workspace",
|
|
258
|
+
source: Extract<SkillCommandSource, "installed-user" | "installed-workspace">,
|
|
259
|
+
): Promise<void> => {
|
|
260
|
+
const installed = await listInstalledSkills({
|
|
261
|
+
location,
|
|
262
|
+
cwd: options.cwd,
|
|
263
|
+
});
|
|
264
|
+
for (const skills of Object.values(installed)) {
|
|
265
|
+
for (const skill of skills) {
|
|
266
|
+
if (skill.sourcePath) {
|
|
267
|
+
const resolved = path.resolve(skill.sourcePath);
|
|
268
|
+
if (!dirs.has(resolved)) {
|
|
269
|
+
dirs.set(resolved, { dir: resolved, source });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
if (options.includeInstalled === "user" || options.includeInstalled === "all") {
|
|
278
|
+
await addInstalled("global", "installed-user");
|
|
279
|
+
}
|
|
280
|
+
if (options.includeInstalled === "workspace" || options.includeInstalled === "all") {
|
|
281
|
+
await addInstalled("workspace", "installed-workspace");
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
diagnostics.push({
|
|
285
|
+
level: "warning",
|
|
286
|
+
message: `Failed to list installed skills: ${err instanceof Error ? err.message : String(err)}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return [...dirs.values()];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function commandFromSkillDir(
|
|
294
|
+
skillDir: string,
|
|
295
|
+
source: DiscoveredSkillSource,
|
|
296
|
+
runtime: string,
|
|
297
|
+
diagnostics: SkillCommandDiagnostic[],
|
|
298
|
+
): Promise<SkillCommandDescriptor | null> {
|
|
299
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
300
|
+
let parsed: ParsedSkillFile;
|
|
301
|
+
try {
|
|
302
|
+
const content = await fs.readFile(skillFile, "utf8");
|
|
303
|
+
parsed = parseSkillFileContent(content);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
diagnostics.push({
|
|
306
|
+
level: "error",
|
|
307
|
+
path: skillFile,
|
|
308
|
+
message: `Failed to read SKILL.md: ${err instanceof Error ? err.message : String(err)}`,
|
|
309
|
+
});
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const name = path.basename(skillDir);
|
|
314
|
+
if (!isValidSkillName(name)) {
|
|
315
|
+
diagnostics.push({
|
|
316
|
+
level: "error",
|
|
317
|
+
path: skillFile,
|
|
318
|
+
message: `Invalid skill command name "${name}"`,
|
|
319
|
+
});
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
id: `${runtime}:${name}:${skillDir}`,
|
|
325
|
+
name,
|
|
326
|
+
description: parsed.frontmatter["description"] ?? firstBodyParagraph(parsed.body),
|
|
327
|
+
argumentHint: parsed.frontmatter["argument-hint"],
|
|
328
|
+
source,
|
|
329
|
+
sourcePath: skillDir,
|
|
330
|
+
userInvocable: parseBoolean(parsed.frontmatter["user-invocable"], true),
|
|
331
|
+
available: true,
|
|
332
|
+
execution: runtime === "claude"
|
|
333
|
+
? { kind: "provider-slash", provider: "claude", commandText: `/${name}` }
|
|
334
|
+
: { kind: "expanded-prompt", provider: runtime },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function parseSkillFileContent(content: string): ParsedSkillFile {
|
|
339
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
340
|
+
if (!normalized.startsWith("---\n")) {
|
|
341
|
+
return { frontmatter: {}, body: normalized.trim() };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const end = normalized.indexOf("\n---", 4);
|
|
345
|
+
if (end === -1) {
|
|
346
|
+
return { frontmatter: {}, body: normalized.trim() };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const frontmatterText = normalized.slice(4, end).trim();
|
|
350
|
+
const bodyStart = normalized.indexOf("\n", end + 1);
|
|
351
|
+
const body = bodyStart === -1 ? "" : normalized.slice(bodyStart + 1).trim();
|
|
352
|
+
return { frontmatter: parseSimpleFrontmatter(frontmatterText), body };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function parseSimpleFrontmatter(input: string): Record<string, string> {
|
|
356
|
+
const values: Record<string, string> = {};
|
|
357
|
+
const lines = input.split("\n");
|
|
358
|
+
let i = 0;
|
|
359
|
+
while (i < lines.length) {
|
|
360
|
+
const line = lines[i] ?? "";
|
|
361
|
+
const trimmed = line.trim();
|
|
362
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
363
|
+
i++;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const idx = trimmed.indexOf(":");
|
|
367
|
+
if (idx <= 0) {
|
|
368
|
+
i++;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const key = trimmed.slice(0, idx).trim();
|
|
372
|
+
const valuePart = trimmed.slice(idx + 1).trim();
|
|
373
|
+
|
|
374
|
+
// YAML block scalars: `description: |` (literal — newlines preserved)
|
|
375
|
+
// or `description: >` (folded — single newlines become spaces). Both
|
|
376
|
+
// optionally followed by `-` (strip trailing newlines) or `+` (keep).
|
|
377
|
+
// The block continues for lines indented strictly more than the key's
|
|
378
|
+
// line; blank lines belong to the block. Indent modifiers (`|2`) and
|
|
379
|
+
// YAML's full quoting rules are deliberately out of scope — skills
|
|
380
|
+
// overwhelmingly use the plain forms.
|
|
381
|
+
const blockMatch = key ? /^([|>])[+-]?\s*$/.exec(valuePart) : null;
|
|
382
|
+
if (blockMatch) {
|
|
383
|
+
const folded = blockMatch[1] === ">";
|
|
384
|
+
const keyIndent = line.length - line.trimStart().length;
|
|
385
|
+
const blockLines: string[] = [];
|
|
386
|
+
let baseIndent: number | null = null;
|
|
387
|
+
let j = i + 1;
|
|
388
|
+
while (j < lines.length) {
|
|
389
|
+
const next = lines[j] ?? "";
|
|
390
|
+
if (next.trim() === "") {
|
|
391
|
+
blockLines.push("");
|
|
392
|
+
j++;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const indent = next.length - next.trimStart().length;
|
|
396
|
+
if (indent <= keyIndent) break;
|
|
397
|
+
if (baseIndent === null) baseIndent = indent;
|
|
398
|
+
blockLines.push(next.slice(Math.min(indent, baseIndent)));
|
|
399
|
+
j++;
|
|
400
|
+
}
|
|
401
|
+
const joiner = folded ? " " : "\n";
|
|
402
|
+
values[key] = blockLines.join(joiner).trim();
|
|
403
|
+
i = j;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (key) values[key] = stripQuotes(valuePart);
|
|
408
|
+
i++;
|
|
409
|
+
}
|
|
410
|
+
return values;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function stripQuotes(value: string): string {
|
|
414
|
+
if (
|
|
415
|
+
(value.startsWith("\"") && value.endsWith("\"")) ||
|
|
416
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
417
|
+
) {
|
|
418
|
+
return value.slice(1, -1);
|
|
419
|
+
}
|
|
420
|
+
return value;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
|
|
424
|
+
if (value === undefined) return fallback;
|
|
425
|
+
const normalized = value.trim().toLowerCase();
|
|
426
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
427
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
428
|
+
return fallback;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function firstBodyParagraph(body: string): string | undefined {
|
|
432
|
+
for (const block of body.split(/\n\s*\n/)) {
|
|
433
|
+
const normalized = block
|
|
434
|
+
.split("\n")
|
|
435
|
+
.map((line) => line.trim().replace(/^#+\s*/, ""))
|
|
436
|
+
.filter(Boolean)
|
|
437
|
+
.join(" ")
|
|
438
|
+
.trim();
|
|
439
|
+
if (normalized) return normalized;
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function isValidSkillName(name: string): boolean {
|
|
445
|
+
return /^[A-Za-z0-9][A-Za-z0-9:_-]*$/.test(name);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function readSkill(sourcePath: string): Promise<{ body: string; skillDir: string }> {
|
|
449
|
+
const stat = await fs.stat(sourcePath);
|
|
450
|
+
const skillFile = stat.isDirectory() ? path.join(sourcePath, "SKILL.md") : sourcePath;
|
|
451
|
+
const skillDir = stat.isDirectory() ? sourcePath : path.dirname(sourcePath);
|
|
452
|
+
const content = await fs.readFile(skillFile, "utf8");
|
|
453
|
+
return { body: parseSkillFileContent(content).body, skillDir };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function realpathOrResolved(dir: string): Promise<string> {
|
|
457
|
+
try {
|
|
458
|
+
return await fs.realpath(dir);
|
|
459
|
+
} catch {
|
|
460
|
+
return path.resolve(dir);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function substituteSkillArguments(body: string, args: string, skillDir?: string): string {
|
|
465
|
+
return body
|
|
466
|
+
.replace(/\$\{ARGUMENTS\}/g, args)
|
|
467
|
+
.replace(/\$ARGUMENTS\b/g, args)
|
|
468
|
+
.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir ?? "")
|
|
469
|
+
.replace(/\{\{\s*args\s*\}\}/g, args)
|
|
470
|
+
.replace(/\{\{\s*ARGUMENTS\s*\}\}/g, args);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function formatCommandText(commandText: string, args?: string): string {
|
|
474
|
+
const trimmedCommand = commandText.trim();
|
|
475
|
+
const trimmedArgs = args?.trim() ?? "";
|
|
476
|
+
return trimmedArgs ? `${trimmedCommand} ${trimmedArgs}` : trimmedCommand;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function readStringArray(raw: unknown, key: string): string[] | undefined {
|
|
480
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return undefined;
|
|
481
|
+
const value = (raw as Record<string, unknown>)[key];
|
|
482
|
+
if (!Array.isArray(value)) return undefined;
|
|
483
|
+
return value.filter((item): item is string => typeof item === "string");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function inventoryCandidatesFor(command: SkillCommandDescriptor): string[] {
|
|
487
|
+
if (command.execution.kind !== "provider-slash") return [command.name];
|
|
488
|
+
const commandText = command.execution.commandText.trim();
|
|
489
|
+
const slashName = commandText
|
|
490
|
+
.split(/\s+/, 1)[0]
|
|
491
|
+
?.replace(/^\/+/, "");
|
|
492
|
+
return [...new Set([command.name, slashName].filter((name): name is string => !!name))];
|
|
493
|
+
}
|