@cmetech/otto 1.0.8 → 1.1.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/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.js +6 -0
- package/dist/cli.js +27 -0
- package/dist/help-text.js +15 -0
- package/dist/onboarding.d.ts +19 -0
- package/dist/onboarding.js +133 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/otto/commands/release-notes/_data.js +171 -0
- package/dist/resources/extensions/otto/commands/release-notes/command.js +114 -0
- package/dist/resources/extensions/otto/commands/theme/command.js +75 -0
- package/dist/resources/extensions/otto/extension-manifest.json +1 -1
- package/dist/resources/extensions/otto/index.js +6 -0
- package/dist/resources/extensions/subagent/agents.js +160 -8
- package/dist/resources/extensions/subagent/index.js +45 -4
- package/dist/resources/extensions/subagent/skill-tool-stub.js +23 -0
- package/dist/seed-defaults.d.ts +66 -0
- package/dist/seed-defaults.js +294 -0
- package/package.json +8 -7
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +3 -3
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +17 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +89 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +105 -67
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js +20 -2
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +28 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +47 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +22 -0
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/package.json +2 -2
- package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +90 -1
- package/packages/pi-coding-agent/src/core/package-manager.ts +131 -64
- package/packages/pi-coding-agent/src/core/resolve-config-value.ts +20 -2
- package/packages/pi-coding-agent/src/core/settings-manager.ts +65 -0
- package/packages/pi-coding-agent/src/core/skills.ts +23 -1
- package/packages/pi-coding-agent/src/index.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -2
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +4 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/autocomplete.d.ts +9 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +2 -0
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/dist/components/select-list.d.ts +10 -0
- package/packages/pi-tui/dist/components/select-list.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/select-list.js +30 -17
- package/packages/pi-tui/dist/components/select-list.js.map +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/src/autocomplete.ts +11 -0
- package/packages/pi-tui/src/components/select-list.ts +41 -17
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +4 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/otto/commands/release-notes/_data.ts +187 -0
- package/src/resources/extensions/otto/commands/release-notes/command.ts +141 -0
- package/src/resources/extensions/otto/commands/theme/command.ts +89 -0
- package/src/resources/extensions/otto/extension-manifest.json +1 -1
- package/src/resources/extensions/otto/index.ts +8 -0
- package/src/resources/extensions/subagent/agents.ts +166 -8
- package/src/resources/extensions/subagent/index.ts +46 -6
- package/src/resources/extensions/subagent/skill-tool-stub.ts +28 -0
- package/src/resources/extensions/subagent/tests/parse-agent-tools.test.ts +52 -0
- package/src/resources/extensions/subagent/tests/skill-tool-stub.test.ts +23 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /theme — list and switch the active OTTO theme at runtime.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* /theme → interactive picker over built-ins + ~/.otto/agent/themes/*.json
|
|
6
|
+
* /theme <name> → switch directly (e.g. /theme cool-mint)
|
|
7
|
+
* /theme list → print a one-line list of available themes
|
|
8
|
+
*
|
|
9
|
+
* Switch is for the current session. To persist across launches, also set
|
|
10
|
+
* `"theme": "<name>"` in ~/.otto/agent/settings.json.
|
|
11
|
+
*/
|
|
12
|
+
import { getAvailableThemesWithPaths, setTheme } from "@otto/pi-coding-agent";
|
|
13
|
+
const CUSTOM_TYPE = "otto-theme";
|
|
14
|
+
function postToChat(pi, content) {
|
|
15
|
+
pi.sendMessage({ customType: CUSTOM_TYPE, content, display: true });
|
|
16
|
+
}
|
|
17
|
+
function renderList() {
|
|
18
|
+
const themes = getAvailableThemesWithPaths();
|
|
19
|
+
const lines = themes.map((t) => {
|
|
20
|
+
const origin = t.path ? ` _(${t.path})_` : " _(built-in)_";
|
|
21
|
+
return `- \`${t.name}\`${origin}`;
|
|
22
|
+
});
|
|
23
|
+
return `**Available themes (${themes.length})**\n\n${lines.join("\n")}\n\nUse \`/theme <name>\` to switch. Persist with \`"theme": "<name>"\` in \`~/.otto/agent/settings.json\`.`;
|
|
24
|
+
}
|
|
25
|
+
function applyTheme(pi, name) {
|
|
26
|
+
const result = setTheme(name);
|
|
27
|
+
if (result.success) {
|
|
28
|
+
postToChat(pi, `**Theme switched to \`${name}\`** for this session.\n\nTo persist, set \`"theme": "${name}"\` in \`~/.otto/agent/settings.json\`.`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const available = getAvailableThemesWithPaths().map((t) => t.name).join(", ");
|
|
32
|
+
postToChat(pi, `**Theme switch failed:** ${result.error ?? "unknown error"}\n\nAvailable: ${available}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function registerThemeCommand(pi) {
|
|
36
|
+
pi.registerCommand("theme", {
|
|
37
|
+
description: "List or switch the active OTTO theme",
|
|
38
|
+
handler: async (args, ctx) => {
|
|
39
|
+
const trimmed = args.trim();
|
|
40
|
+
if (trimmed === "list") {
|
|
41
|
+
postToChat(pi, renderList());
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (trimmed) {
|
|
45
|
+
applyTheme(pi, trimmed);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Interactive picker
|
|
49
|
+
if (!ctx.hasUI || typeof ctx.ui?.select !== "function") {
|
|
50
|
+
postToChat(pi, renderList());
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const themes = getAvailableThemesWithPaths();
|
|
54
|
+
const options = themes.map((t) => (t.path ? `${t.name} (custom)` : `${t.name} (built-in)`));
|
|
55
|
+
let pick;
|
|
56
|
+
try {
|
|
57
|
+
pick = await ctx.ui.select(`Choose theme — ${themes.length} available`, options);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
postToChat(pi, `**theme picker error:** ${err.message}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!pick)
|
|
64
|
+
return;
|
|
65
|
+
const picked = Array.isArray(pick) ? pick[0] : pick;
|
|
66
|
+
const index = options.indexOf(picked);
|
|
67
|
+
const chosen = index >= 0 ? themes[index] : undefined;
|
|
68
|
+
if (!chosen) {
|
|
69
|
+
postToChat(pi, `**No match for** \`${picked}\``);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
applyTheme(pi, chosen.name);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -19,6 +19,8 @@ import { registerOttoTools } from "./tools/_loader.js";
|
|
|
19
19
|
import { executeLangFlowTool } from "./tools/langflow.js";
|
|
20
20
|
import { registerBuildFlowCommand } from "./commands/build-flow/command.js";
|
|
21
21
|
import { registerPromptEngineerCommand } from "./commands/prompt-engineer/command.js";
|
|
22
|
+
import { registerReleaseNotesCommand } from "./commands/release-notes/command.js";
|
|
23
|
+
import { registerThemeCommand } from "./commands/theme/command.js";
|
|
22
24
|
import { parseLangFlowNaturalLanguage } from "./commands/langflow/natural-language.js";
|
|
23
25
|
const _here = dirname(fileURLToPath(import.meta.url));
|
|
24
26
|
const FLOW_TRIGGERS_DIR = join(_here, "commands", "flow-triggers");
|
|
@@ -182,6 +184,10 @@ export default function Otto(pi) {
|
|
|
182
184
|
registerBuildFlowCommand(pi);
|
|
183
185
|
// ── Register /otto prompt-engineer slash command (Phase 5) ──
|
|
184
186
|
registerPromptEngineerCommand(pi);
|
|
187
|
+
// ── Register /release-notes slash command ──
|
|
188
|
+
registerReleaseNotesCommand(pi);
|
|
189
|
+
// ── Register /theme slash command ──
|
|
190
|
+
registerThemeCommand(pi);
|
|
185
191
|
// ── Load and register flow-trigger slash commands ──
|
|
186
192
|
// Fire-and-forget. Pi's command registry is dynamic; late registrations work.
|
|
187
193
|
loadFlowTriggers(FLOW_TRIGGERS_DIR)
|
|
@@ -2,33 +2,113 @@
|
|
|
2
2
|
* Agent discovery and configuration
|
|
3
3
|
*/
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
5
6
|
import * as path from "node:path";
|
|
6
7
|
import { getAgentDir, parseFrontmatter } from "@otto/pi-coding-agent";
|
|
7
8
|
const PROJECT_AGENT_DIR_CANDIDATES = [".otto/workflow", ".pi"];
|
|
9
|
+
/**
|
|
10
|
+
* Conventional agent folders used by other AI coding harnesses, mirroring the
|
|
11
|
+
* harness skill-paths support in pi-coding-agent's skills.ts. Each entry maps
|
|
12
|
+
* a harness id (used as a discriminator in logs and the agent's `source`
|
|
13
|
+
* label) to its user-scope and project-scope conventional paths.
|
|
14
|
+
*
|
|
15
|
+
* When a Claude-style skill delegates to a subagent (`subagent_type: foo` /
|
|
16
|
+
* `Task` tool call), OTTO's subagent tool resolves `foo` against everything
|
|
17
|
+
* `discoverAgents` returns. Including these harness paths is what lets a
|
|
18
|
+
* skill imported from `~/.claude/skills` find its companion agent in
|
|
19
|
+
* `~/.claude/agents` rather than failing with "unknown agent."
|
|
20
|
+
*
|
|
21
|
+
* Caveats — independent of discovery — that may affect runtime success:
|
|
22
|
+
* - Claude agents commonly declare `tools: [Bash, Read, ...]` (capitalized).
|
|
23
|
+
* OTTO's tool names tend to be lowercase. If an agent's allowlist is
|
|
24
|
+
* enforced strictly by the harness it was written for, capitalized
|
|
25
|
+
* entries won't match OTTO's tool registry. Agents without a `tools`
|
|
26
|
+
* field (no restriction) work without issue.
|
|
27
|
+
* - Agent body prompts may reference harness-specific features
|
|
28
|
+
* (Claude's `/compact`, MCP server names hardcoded for `claude_desktop`,
|
|
29
|
+
* `~/.claude/...` paths). The agent still runs; those references may
|
|
30
|
+
* just be ineffective.
|
|
31
|
+
*/
|
|
32
|
+
const HARNESS_AGENT_PATHS = {
|
|
33
|
+
claude: {
|
|
34
|
+
userDir: path.join(homedir(), ".claude", "agents"),
|
|
35
|
+
projectSubdir: path.join(".claude", "agents"),
|
|
36
|
+
},
|
|
37
|
+
codex: {
|
|
38
|
+
userDir: path.join(homedir(), ".codex", "agents"),
|
|
39
|
+
projectSubdir: path.join(".codex", "agents"),
|
|
40
|
+
},
|
|
41
|
+
kiro: {
|
|
42
|
+
userDir: path.join(homedir(), ".kiro", "agents"),
|
|
43
|
+
projectSubdir: path.join(".kiro", "agents"),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
8
46
|
export function parseConflictsWith(value) {
|
|
9
47
|
if (typeof value !== "string")
|
|
10
48
|
return undefined;
|
|
11
49
|
const conflicts = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
12
50
|
return conflicts.length > 0 ? conflicts : undefined;
|
|
13
51
|
}
|
|
14
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Maps capitalized tool names used by other AI coding harnesses (Claude, Codex,
|
|
54
|
+
* Kiro) to OTTO's lowercase tool-registry names. Applied at agent-load time so
|
|
55
|
+
* an agent imported from ~/.claude/agents/ with `tools: [Bash, Read]` ends up
|
|
56
|
+
* with `tools: ["bash", "read"]` and actually has access to those tools.
|
|
57
|
+
*
|
|
58
|
+
* Tools without an explicit mapping fall through to .toLowerCase() — that's
|
|
59
|
+
* enough for any name that's already aligned with OTTO's case-insensitive
|
|
60
|
+
* convention. Tool names with no OTTO equivalent (TodoWrite, SlashCommand,
|
|
61
|
+
* NotebookEdit) are kept (lowercased) and silently ignored by the runtime
|
|
62
|
+
* allowlist check — so they don't block the rest of the agent's toolset.
|
|
63
|
+
*
|
|
64
|
+
* MCP tool patterns (mcp__server__name, mcp__server__*) are preserved
|
|
65
|
+
* verbatim — they're already case-sensitive identifiers, and MCP servers
|
|
66
|
+
* register their own tools at runtime.
|
|
67
|
+
*/
|
|
68
|
+
const HARNESS_TOOL_NAME_MAP = {
|
|
69
|
+
Bash: "bash",
|
|
70
|
+
Read: "read",
|
|
71
|
+
Write: "write",
|
|
72
|
+
Edit: "edit",
|
|
73
|
+
Glob: "glob",
|
|
74
|
+
Grep: "grep",
|
|
75
|
+
AskUserQuestion: "ask_user_questions",
|
|
76
|
+
Agent: "subagent",
|
|
77
|
+
Task: "subagent",
|
|
78
|
+
WebSearch: "web_search",
|
|
79
|
+
WebFetch: "fetch_page",
|
|
80
|
+
Skill: "skill",
|
|
81
|
+
};
|
|
82
|
+
function normalizeHarnessToolName(raw) {
|
|
83
|
+
const trimmed = raw.trim();
|
|
84
|
+
if (!trimmed)
|
|
85
|
+
return null;
|
|
86
|
+
if (trimmed.startsWith("mcp__"))
|
|
87
|
+
return trimmed;
|
|
88
|
+
if (HARNESS_TOOL_NAME_MAP[trimmed])
|
|
89
|
+
return HARNESS_TOOL_NAME_MAP[trimmed];
|
|
90
|
+
return trimmed.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
export function parseAgentTools(value) {
|
|
15
93
|
if (typeof value === "string") {
|
|
16
94
|
const tools = value
|
|
17
95
|
.split(",")
|
|
18
|
-
.map((tool) => tool
|
|
19
|
-
.filter(Boolean);
|
|
20
|
-
|
|
96
|
+
.map((tool) => normalizeHarnessToolName(tool))
|
|
97
|
+
.filter((tool) => Boolean(tool));
|
|
98
|
+
const deduped = Array.from(new Set(tools));
|
|
99
|
+
return deduped.length > 0 ? deduped : undefined;
|
|
21
100
|
}
|
|
22
101
|
if (Array.isArray(value)) {
|
|
23
102
|
const tools = value
|
|
24
103
|
.flatMap((tool) => typeof tool === "string" ? tool.split(",") : [])
|
|
25
|
-
.map((tool) => tool
|
|
26
|
-
.filter(Boolean);
|
|
27
|
-
|
|
104
|
+
.map((tool) => normalizeHarnessToolName(tool))
|
|
105
|
+
.filter((tool) => Boolean(tool));
|
|
106
|
+
const deduped = Array.from(new Set(tools));
|
|
107
|
+
return deduped.length > 0 ? deduped : undefined;
|
|
28
108
|
}
|
|
29
109
|
return undefined;
|
|
30
110
|
}
|
|
31
|
-
function loadAgentsFromDir(dir, source) {
|
|
111
|
+
function loadAgentsFromDir(dir, source, harnessSource) {
|
|
32
112
|
const agents = [];
|
|
33
113
|
if (!fs.existsSync(dir)) {
|
|
34
114
|
return agents;
|
|
@@ -68,6 +148,7 @@ function loadAgentsFromDir(dir, source) {
|
|
|
68
148
|
systemPrompt: body,
|
|
69
149
|
source,
|
|
70
150
|
filePath,
|
|
151
|
+
harnessSource,
|
|
71
152
|
});
|
|
72
153
|
}
|
|
73
154
|
return agents;
|
|
@@ -96,25 +177,96 @@ function findNearestProjectAgentsDir(cwd) {
|
|
|
96
177
|
currentDir = parentDir;
|
|
97
178
|
}
|
|
98
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Walk up from cwd looking for harness project agent dirs (e.g. `.claude/agents`).
|
|
182
|
+
* Returns ALL matching dirs found at the first ancestor that has at least one,
|
|
183
|
+
* so a project root with both `.claude/agents` and `.codex/agents` surfaces
|
|
184
|
+
* both. Stops at the first ancestor with any matches to avoid pulling in
|
|
185
|
+
* stale agents from outer directories.
|
|
186
|
+
*/
|
|
187
|
+
function findNearestHarnessProjectAgentsDirs(cwd) {
|
|
188
|
+
let currentDir = cwd;
|
|
189
|
+
while (true) {
|
|
190
|
+
const matches = [];
|
|
191
|
+
for (const { projectSubdir } of Object.values(HARNESS_AGENT_PATHS)) {
|
|
192
|
+
const candidate = path.join(currentDir, projectSubdir);
|
|
193
|
+
if (isDirectory(candidate))
|
|
194
|
+
matches.push(candidate);
|
|
195
|
+
}
|
|
196
|
+
if (matches.length > 0)
|
|
197
|
+
return matches;
|
|
198
|
+
const parentDir = path.dirname(currentDir);
|
|
199
|
+
if (parentDir === currentDir)
|
|
200
|
+
return [];
|
|
201
|
+
currentDir = parentDir;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
99
204
|
export function discoverAgents(cwd, scope) {
|
|
100
205
|
const userDir = path.join(getAgentDir(), "agents");
|
|
101
206
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
207
|
+
const harnessProjectAgentsDirs = findNearestHarnessProjectAgentsDirs(cwd);
|
|
102
208
|
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
103
209
|
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
210
|
+
// Harness agents — user scope: ~/.claude/agents, ~/.codex/agents, ~/.kiro/agents.
|
|
211
|
+
// Harness agents — project scope: nearest .claude/agents etc. up the cwd tree.
|
|
212
|
+
// Both go into the same scope buckets as OTTO's own agents; collisions are
|
|
213
|
+
// resolved by the existing Map-write-wins order below (project wins user).
|
|
214
|
+
const harnessUserAgents = [];
|
|
215
|
+
if (scope !== "project") {
|
|
216
|
+
for (const [harnessId, { userDir: harnessUserDir }] of Object.entries(HARNESS_AGENT_PATHS)) {
|
|
217
|
+
harnessUserAgents.push(...loadAgentsFromDir(harnessUserDir, "user", harnessId));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const harnessProjectAgents = [];
|
|
221
|
+
if (scope !== "user") {
|
|
222
|
+
// Map each discovered project dir back to its harness id by suffix match
|
|
223
|
+
// (e.g. ".../.claude/agents" → "claude"). The id surfaces in /subagent as
|
|
224
|
+
// a glanceable chip.
|
|
225
|
+
for (const dir of harnessProjectAgentsDirs) {
|
|
226
|
+
let harnessId;
|
|
227
|
+
for (const [id, { projectSubdir }] of Object.entries(HARNESS_AGENT_PATHS)) {
|
|
228
|
+
if (dir.endsWith(projectSubdir)) {
|
|
229
|
+
harnessId = id;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
harnessProjectAgents.push(...loadAgentsFromDir(dir, "project", harnessId));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
104
236
|
const agentMap = new Map();
|
|
237
|
+
// Order: OTTO user → harness user → OTTO project → harness project.
|
|
238
|
+
// Later writes win on name collision, so OTTO's own agents take precedence
|
|
239
|
+
// at each scope, and project always overrides user. Same precedence as the
|
|
240
|
+
// existing logic, just with harness sources slotted in beneath.
|
|
105
241
|
if (scope === "both") {
|
|
106
242
|
for (const agent of userAgents)
|
|
107
243
|
agentMap.set(agent.name, agent);
|
|
244
|
+
for (const agent of harnessUserAgents) {
|
|
245
|
+
if (!agentMap.has(agent.name))
|
|
246
|
+
agentMap.set(agent.name, agent);
|
|
247
|
+
}
|
|
108
248
|
for (const agent of projectAgents)
|
|
109
249
|
agentMap.set(agent.name, agent);
|
|
250
|
+
for (const agent of harnessProjectAgents) {
|
|
251
|
+
if (!agentMap.has(agent.name))
|
|
252
|
+
agentMap.set(agent.name, agent);
|
|
253
|
+
}
|
|
110
254
|
}
|
|
111
255
|
else if (scope === "user") {
|
|
112
256
|
for (const agent of userAgents)
|
|
113
257
|
agentMap.set(agent.name, agent);
|
|
258
|
+
for (const agent of harnessUserAgents) {
|
|
259
|
+
if (!agentMap.has(agent.name))
|
|
260
|
+
agentMap.set(agent.name, agent);
|
|
261
|
+
}
|
|
114
262
|
}
|
|
115
263
|
else {
|
|
116
264
|
for (const agent of projectAgents)
|
|
117
265
|
agentMap.set(agent.name, agent);
|
|
266
|
+
for (const agent of harnessProjectAgents) {
|
|
267
|
+
if (!agentMap.has(agent.name))
|
|
268
|
+
agentMap.set(agent.name, agent);
|
|
269
|
+
}
|
|
118
270
|
}
|
|
119
271
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
120
272
|
}
|
|
@@ -17,12 +17,13 @@ import * as fs from "node:fs";
|
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { StringEnum } from "@otto/pi-ai";
|
|
20
|
-
import { getMarkdownTheme } from "@otto/pi-coding-agent";
|
|
20
|
+
import { getMarkdownTheme, getSelectListTheme } from "@otto/pi-coding-agent";
|
|
21
21
|
import { Container, Markdown, Spacer, Text } from "@otto/pi-tui";
|
|
22
22
|
import { Type } from "@sinclair/typebox";
|
|
23
23
|
import { formatTokenCount } from "../shared/mod.js";
|
|
24
24
|
import { getCurrentPhase } from "../shared/phase-state.js";
|
|
25
25
|
import { discoverAgents } from "./agents.js";
|
|
26
|
+
import { buildSkillToolStubResponse } from "./skill-tool-stub.js";
|
|
26
27
|
import { createIsolation, mergeDeltaPatches, readIsolationMode, } from "./isolation.js";
|
|
27
28
|
import { registerWorker, updateWorker } from "./worker-registry.js";
|
|
28
29
|
import { loadEffectiveGSDPreferences } from "../workflow/preferences.js";
|
|
@@ -643,7 +644,13 @@ export default function (pi) {
|
|
|
643
644
|
pi.on("session_shutdown", async () => {
|
|
644
645
|
await stopLiveSubagents();
|
|
645
646
|
});
|
|
646
|
-
// /subagent command - list available agents
|
|
647
|
+
// /subagent command - list available agents.
|
|
648
|
+
// Renders each row with embedded ANSI styling so the message bypasses the
|
|
649
|
+
// notify renderer's "dim everything" fallback (see hasAnsiStyling check in
|
|
650
|
+
// pi-coding-agent's renderExtensionNotifyInChat). Agent name renders in the
|
|
651
|
+
// terminal's default foreground (white-ish), source/model in the muted
|
|
652
|
+
// description tint, and the harness origin — when detected — as an accent
|
|
653
|
+
// chip matching the /skills autocomplete style.
|
|
647
654
|
pi.registerCommand("subagent", {
|
|
648
655
|
description: "List available subagents",
|
|
649
656
|
handler: async (_args, ctx) => {
|
|
@@ -652,8 +659,17 @@ export default function (pi) {
|
|
|
652
659
|
ctx.ui.notify("No agents found. Add .md files to ~/.otto/agent/agents/ or .otto/workflow/agents/", "warning");
|
|
653
660
|
return;
|
|
654
661
|
}
|
|
655
|
-
const
|
|
656
|
-
|
|
662
|
+
const slt = getSelectListTheme();
|
|
663
|
+
const tag = slt.tag ?? slt.selectedText;
|
|
664
|
+
const dim = slt.description;
|
|
665
|
+
const lines = discovery.agents.map((a) => {
|
|
666
|
+
const chip = a.harnessSource ? `${tag(`[${a.harnessSource}]`)} ` : "";
|
|
667
|
+
const meta = dim(`[${a.source}]${a.model ? ` (${a.model})` : ""}`);
|
|
668
|
+
const desc = dim(`: ${a.description}`);
|
|
669
|
+
return ` ${chip}${a.name} ${meta}${desc}`;
|
|
670
|
+
});
|
|
671
|
+
const header = dim(`Available agents (${discovery.agents.length}):`);
|
|
672
|
+
ctx.ui.notify(`${header}\n${lines.join("\n")}`, "info");
|
|
657
673
|
},
|
|
658
674
|
});
|
|
659
675
|
pi.registerTool({
|
|
@@ -1565,4 +1581,29 @@ export default function (pi) {
|
|
|
1565
1581
|
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1566
1582
|
},
|
|
1567
1583
|
});
|
|
1584
|
+
pi.registerTool({
|
|
1585
|
+
name: "skill",
|
|
1586
|
+
label: "Skill (stub)",
|
|
1587
|
+
description: [
|
|
1588
|
+
"Stub for the Claude-style `Skill` tool used by some imported skills.",
|
|
1589
|
+
"OTTO does not support invoking skills as a tool — users invoke skills via /skill:<name> from the chat input.",
|
|
1590
|
+
"Returns a clear message redirecting the model to use the skill content inline.",
|
|
1591
|
+
].join(" "),
|
|
1592
|
+
parameters: Type.Object({
|
|
1593
|
+
name: Type.Optional(Type.String({ description: "Name of the skill the model wanted to invoke." })),
|
|
1594
|
+
}),
|
|
1595
|
+
async execute(_toolCallId, params) {
|
|
1596
|
+
return {
|
|
1597
|
+
content: [
|
|
1598
|
+
{
|
|
1599
|
+
type: "text",
|
|
1600
|
+
text: buildSkillToolStubResponse({
|
|
1601
|
+
name: typeof params.name === "string" ? params.name : undefined,
|
|
1602
|
+
}),
|
|
1603
|
+
},
|
|
1604
|
+
],
|
|
1605
|
+
details: {},
|
|
1606
|
+
};
|
|
1607
|
+
},
|
|
1608
|
+
});
|
|
1568
1609
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stub for the `skill` tool that Claude-style skills sometimes invoke
|
|
3
|
+
* (`Skill(name="foo")` in the skill body) to chain into another skill.
|
|
4
|
+
*
|
|
5
|
+
* OTTO does not support model-invoked skill execution — skills are user-
|
|
6
|
+
* initiated via `/skill:<name>` from the chat input. Rather than have those
|
|
7
|
+
* tool calls fail with "no such tool," we register a stub that returns a
|
|
8
|
+
* clear, actionable message. The model sees the response and can either
|
|
9
|
+
* fall back to acting on the skill content itself or surface the message
|
|
10
|
+
* to the user.
|
|
11
|
+
*
|
|
12
|
+
* Registered in src/resources/extensions/subagent/index.ts.
|
|
13
|
+
*/
|
|
14
|
+
export function buildSkillToolStubResponse(input) {
|
|
15
|
+
const skillRef = typeof input.name === "string" && input.name.trim().length > 0
|
|
16
|
+
? `/skill:${input.name.trim()}`
|
|
17
|
+
: "the desired /skill:<name>";
|
|
18
|
+
return [
|
|
19
|
+
"OTTO does not support invoking skills as a tool from inside an agent turn.",
|
|
20
|
+
`To use this skill, ask the user to run \`${skillRef}\` from the chat input — that's how OTTO surfaces skills.`,
|
|
21
|
+
"If you have access to the skill's body (it was loaded as a system prompt), follow its instructions inline instead.",
|
|
22
|
+
].join("\n\n");
|
|
23
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conventional skill folders used by other AI coding harnesses. When the
|
|
3
|
+
* recommended-packages seeding runs and the directory exists on disk, the
|
|
4
|
+
* path string (with literal `~/` so it stays portable in settings.json) is
|
|
5
|
+
* appended to `settings.skills` and recorded in `settings.seededSkillPaths`
|
|
6
|
+
* as a zombie-resurrection guard. Users who remove an entry from
|
|
7
|
+
* settings.skills will not see it re-added on the next launch.
|
|
8
|
+
*
|
|
9
|
+
* Pi.dev's documented convention is for users to add these paths manually to
|
|
10
|
+
* settings.skills; OTTO seeds them automatically as a quality-of-life default
|
|
11
|
+
* for users who have these harnesses installed.
|
|
12
|
+
*/
|
|
13
|
+
export declare const HARNESS_SKILL_PATHS: readonly {
|
|
14
|
+
setting: string;
|
|
15
|
+
resolved: string;
|
|
16
|
+
}[];
|
|
17
|
+
export interface DefaultPackage {
|
|
18
|
+
/** Install source as parsed by package-manager (npm:..., git:..., ./...). */
|
|
19
|
+
source: string;
|
|
20
|
+
/** Short display name shown in onboarding (without the npm: prefix). */
|
|
21
|
+
name: string;
|
|
22
|
+
/** One-line blurb shown as a hint in onboarding's checkbox UI. */
|
|
23
|
+
description: string;
|
|
24
|
+
/**
|
|
25
|
+
* When set, this substring is appended to settings.quietExtensions on first
|
|
26
|
+
* seed so the package's session_start banner is silenced by default. Tracked
|
|
27
|
+
* in settings.seededQuietPatterns so a user who removes the pattern from
|
|
28
|
+
* quietExtensions is not overridden on subsequent launches.
|
|
29
|
+
*
|
|
30
|
+
* Use for packages whose maintainers emit unactionable startup chatter
|
|
31
|
+
* (e.g. pi-notion's `[notion] MCP config found …`).
|
|
32
|
+
*/
|
|
33
|
+
quietPattern?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface DefaultPackageCategory {
|
|
36
|
+
/** Stable id stored in settings.json:enabledDefaultCategories (future). */
|
|
37
|
+
id: string;
|
|
38
|
+
/** Display label shown in the category multiselect. */
|
|
39
|
+
label: string;
|
|
40
|
+
/** One-line description shown as a hint on the category multiselect. */
|
|
41
|
+
description: string;
|
|
42
|
+
/** Packages included in this category. May be empty for placeholder personas. */
|
|
43
|
+
packages: DefaultPackage[];
|
|
44
|
+
}
|
|
45
|
+
export declare const OTTO_DEFAULT_PACKAGE_CATEGORIES: readonly DefaultPackageCategory[];
|
|
46
|
+
/**
|
|
47
|
+
* Flat, deduplicated list of every default source across all categories. Used
|
|
48
|
+
* as the back-compat seeding set when no `enabledDefaultPackages` filter is
|
|
49
|
+
* stored (the flag/env users who never went through the categorical
|
|
50
|
+
* onboarding). The `Set` dedupes when the same source legitimately appears in
|
|
51
|
+
* multiple categories (e.g. a productivity tool that's also useful for devs).
|
|
52
|
+
*/
|
|
53
|
+
export declare const OTTO_DEFAULT_PACKAGES: readonly string[];
|
|
54
|
+
/** Categories with at least one package — the only ones worth showing in UI. */
|
|
55
|
+
export declare function getOnboardableCategories(): DefaultPackageCategory[];
|
|
56
|
+
export interface SeedFlags {
|
|
57
|
+
withDefaults?: boolean;
|
|
58
|
+
noSeedDefaults?: boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare function shouldSeedDefaults(flags: SeedFlags, settingValue: boolean | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Resolves the effective set of sources to seed given the user's enabledDefaultPackages
|
|
63
|
+
* preference. See the file header for semantics of undefined / [] / [...].
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveEnabledDefaults(enabled: string[] | undefined, all?: readonly string[]): string[];
|
|
66
|
+
export declare function maybeSeedDefaultPackages(flags: SeedFlags, agentDirPath: string): Promise<void>;
|