@soleri/forge 5.10.0 → 5.12.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.
Files changed (53) hide show
  1. package/dist/facades/forge.facade.js +3 -3
  2. package/dist/facades/forge.facade.js.map +1 -1
  3. package/dist/lib.d.ts +2 -2
  4. package/dist/lib.js +1 -1
  5. package/dist/lib.js.map +1 -1
  6. package/dist/scaffolder.js +137 -20
  7. package/dist/scaffolder.js.map +1 -1
  8. package/dist/templates/agents-md.d.ts +5 -0
  9. package/dist/templates/agents-md.js +33 -0
  10. package/dist/templates/agents-md.js.map +1 -0
  11. package/dist/templates/entry-point.js +15 -2
  12. package/dist/templates/entry-point.js.map +1 -1
  13. package/dist/templates/package-json.js +7 -0
  14. package/dist/templates/package-json.js.map +1 -1
  15. package/dist/templates/readme.js +80 -27
  16. package/dist/templates/readme.js.map +1 -1
  17. package/dist/templates/setup-script.d.ts +1 -1
  18. package/dist/templates/setup-script.js +135 -53
  19. package/dist/templates/setup-script.js.map +1 -1
  20. package/dist/templates/skills.d.ts +0 -7
  21. package/dist/templates/skills.js +1 -24
  22. package/dist/templates/skills.js.map +1 -1
  23. package/dist/templates/telegram-agent.d.ts +6 -0
  24. package/dist/templates/telegram-agent.js +212 -0
  25. package/dist/templates/telegram-agent.js.map +1 -0
  26. package/dist/templates/telegram-bot.d.ts +6 -0
  27. package/dist/templates/telegram-bot.js +450 -0
  28. package/dist/templates/telegram-bot.js.map +1 -0
  29. package/dist/templates/telegram-config.d.ts +6 -0
  30. package/dist/templates/telegram-config.js +81 -0
  31. package/dist/templates/telegram-config.js.map +1 -0
  32. package/dist/templates/telegram-supervisor.d.ts +6 -0
  33. package/dist/templates/telegram-supervisor.js +148 -0
  34. package/dist/templates/telegram-supervisor.js.map +1 -0
  35. package/dist/types.d.ts +13 -5
  36. package/dist/types.js +7 -1
  37. package/dist/types.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/__tests__/scaffolder.test.ts +62 -0
  40. package/src/facades/forge.facade.ts +3 -3
  41. package/src/lib.ts +2 -1
  42. package/src/scaffolder.ts +170 -28
  43. package/src/templates/agents-md.ts +35 -0
  44. package/src/templates/entry-point.ts +15 -2
  45. package/src/templates/package-json.ts +7 -0
  46. package/src/templates/readme.ts +89 -27
  47. package/src/templates/setup-script.ts +141 -54
  48. package/src/templates/skills.ts +0 -23
  49. package/src/templates/telegram-agent.ts +214 -0
  50. package/src/templates/telegram-bot.ts +452 -0
  51. package/src/templates/telegram-config.ts +83 -0
  52. package/src/templates/telegram-supervisor.ts +150 -0
  53. package/src/types.ts +9 -2
@@ -2,51 +2,24 @@ import type { AgentConfig } from '../types.js';
2
2
 
3
3
  /**
4
4
  * Generate a scripts/setup.sh for the scaffolded agent.
5
- * Handles: Node.js check, build, Claude Code MCP server registration.
5
+ * Handles: Node.js check, build, and host-specific MCP registration.
6
6
  */
7
7
  export function generateSetupScript(config: AgentConfig): string {
8
- return `#!/usr/bin/env bash
9
- set -euo pipefail
10
-
11
- AGENT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
12
- AGENT_NAME="${config.id}"
13
-
14
- echo "=== ${config.name} Setup ==="
15
- echo ""
16
-
17
- # Check Node.js
18
- if ! command -v node &>/dev/null; then
19
- echo "Error: Node.js is not installed. Install Node.js 18+ first."
20
- exit 1
21
- fi
22
-
23
- NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
24
- if [ "$NODE_VERSION" -lt 18 ]; then
25
- echo "Error: Node.js 18+ required (found v$(node -v))."
26
- exit 1
27
- fi
28
- echo "[ok] Node.js $(node -v)"
29
-
30
- # Check if built
31
- if [ ! -f "$AGENT_DIR/dist/index.js" ]; then
32
- echo ""
33
- echo "Building ${config.name}..."
34
- cd "$AGENT_DIR"
35
- npm install
36
- npm run build
37
- echo "[ok] Built successfully"
38
- else
39
- echo "[ok] Already built"
40
- fi
41
-
8
+ const setupTarget = config.setupTarget ?? 'claude';
9
+ const claudeSetup = setupTarget === 'claude' || setupTarget === 'both';
10
+ const codexSetup = setupTarget === 'codex' || setupTarget === 'both';
11
+ const hostLabel =
12
+ claudeSetup && codexSetup ? 'Claude Code + Codex' : claudeSetup ? 'Claude Code' : 'Codex';
13
+
14
+ const claudeSection = claudeSetup
15
+ ? `
42
16
  # Check Claude Code
43
17
  if ! command -v claude &>/dev/null; then
44
18
  echo ""
45
19
  echo "Warning: 'claude' command not found."
46
20
  echo "Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code"
47
21
  echo ""
48
- echo "After installing, add ${config.name} manually to ~/.claude/settings.json"
49
- echo "(see README.md for the JSON config)"
22
+ echo "After installing, re-run this setup script."
50
23
  exit 1
51
24
  fi
52
25
  echo "[ok] Claude Code found"
@@ -54,14 +27,13 @@ echo "[ok] Claude Code found"
54
27
  # Register MCP server with Claude Code
55
28
  echo ""
56
29
  echo "Registering ${config.name} with Claude Code..."
57
-
58
30
  claude mcp add --scope user "$AGENT_NAME" -- node "$AGENT_DIR/dist/index.js"
59
- echo "[ok] Registered ${config.name} as MCP server"
31
+ echo "[ok] Registered ${config.name} as MCP server (Claude Code)"
60
32
 
61
33
  # Configure PreCompact hook for session capture
62
34
  SETTINGS_FILE="$HOME/.claude/settings.json"
63
35
  echo ""
64
- echo "Configuring session capture hook..."
36
+ echo "Configuring Claude session capture hook..."
65
37
 
66
38
  if [ ! -d "$HOME/.claude" ]; then
67
39
  mkdir -p "$HOME/.claude"
@@ -85,7 +57,6 @@ else
85
57
  if grep -q "PreCompact" "$SETTINGS_FILE" 2>/dev/null; then
86
58
  echo "[ok] PreCompact hook already configured — skipping"
87
59
  else
88
- # Use node to safely merge hook into existing settings
89
60
  node -e "
90
61
  const fs = require('fs');
91
62
  const settings = JSON.parse(fs.readFileSync('$SETTINGS_FILE', 'utf-8'));
@@ -107,7 +78,7 @@ COMMANDS_DIR="$HOME/.claude/commands"
107
78
 
108
79
  if [ -d "$SKILLS_DIR" ]; then
109
80
  echo ""
110
- echo "Installing skills..."
81
+ echo "Installing skills for Claude Code..."
111
82
  mkdir -p "$COMMANDS_DIR"
112
83
  skill_installed=0
113
84
  skill_skipped=0
@@ -124,12 +95,15 @@ if [ -d "$SKILLS_DIR" ]; then
124
95
  skill_installed=$((skill_installed + 1))
125
96
  fi
126
97
  done
127
- echo "[ok] Skills: $skill_installed installed, $skill_skipped already present"
98
+ echo "[ok] Claude skills: $skill_installed installed, $skill_skipped already present"
128
99
  fi
100
+ `
101
+ : '';
129
102
 
130
- ${
131
- config.hookPacks?.length
132
- ? `# Install hook packs to global ~/.claude/
103
+ const hookPackSection =
104
+ claudeSetup && config.hookPacks?.length
105
+ ? `
106
+ # Install hook packs to global ~/.claude/
133
107
  AGENT_CLAUDE_DIR="$AGENT_DIR/.claude"
134
108
  GLOBAL_CLAUDE_DIR="$HOME/.claude"
135
109
 
@@ -151,16 +125,129 @@ if [ -d "$AGENT_CLAUDE_DIR" ]; then
151
125
  done
152
126
  echo "[ok] Hooks: $installed installed, $skipped already present"
153
127
  fi
154
-
155
128
  `
156
- : ''
157
- }echo ""
158
- echo "=== Setup Complete ==="
129
+ : '';
130
+
131
+ const codexSection = codexSetup
132
+ ? `
133
+ # Register MCP server with Codex
159
134
  echo ""
160
- echo "Next:"
161
- echo " 1. Start a new Claude Code session (or restart if one is open)"
162
- echo " 2. Say: \\"Hello, ${config.name}!\\""
135
+ echo "Registering ${config.name} with Codex..."
136
+ mkdir -p "$HOME/.codex"
137
+ CODEX_CONFIG="$HOME/.codex/config.toml"
138
+ AGENT_DIST="$AGENT_DIR/dist/index.js"
139
+
140
+ CODEX_CONFIG="$CODEX_CONFIG" AGENT_NAME="$AGENT_NAME" AGENT_DIST="$AGENT_DIST" node <<'NODE'
141
+ const fs = require('node:fs');
142
+ const path = process.env.CODEX_CONFIG;
143
+ const agentName = process.env.AGENT_NAME;
144
+ const distPath = process.env.AGENT_DIST;
145
+
146
+ let content = '';
147
+ if (fs.existsSync(path)) {
148
+ content = fs.readFileSync(path, 'utf-8');
149
+ }
150
+
151
+ const header = '[mcp_servers.' + agentName + ']';
152
+ const block = header + '\\ncommand = "node"\\nargs = ["' + distPath + '"]\\n';
153
+ const start = content.indexOf(header);
154
+
155
+ if (start === -1) {
156
+ const trimmed = content.trimEnd();
157
+ content = trimmed.length === 0 ? block + '\\n' : trimmed + '\\n\\n' + block + '\\n';
158
+ } else {
159
+ const afterHeader = start + header.length;
160
+ const tail = content.slice(afterHeader);
161
+ const nextSectionOffset = tail.search(/\\n\\[[^\\]]+\\]/);
162
+ const end = nextSectionOffset === -1 ? content.length : afterHeader + nextSectionOffset;
163
+ content = content.slice(0, start).trimEnd() + '\\n\\n' + block + '\\n' + content.slice(end).trimStart();
164
+ }
165
+
166
+ content = content.replace(/\\n{3,}/g, '\\n\\n');
167
+ fs.writeFileSync(path, content, 'utf-8');
168
+ NODE
169
+ echo "[ok] Registered ${config.name} as MCP server (Codex)"
170
+
171
+ # Install skills to ~/.codex/skills/
172
+ SKILLS_DIR="$AGENT_DIR/skills"
173
+ CODEX_SKILLS_DIR="$HOME/.codex/skills"
174
+
175
+ if [ -d "$SKILLS_DIR" ]; then
176
+ echo ""
177
+ echo "Installing skills for Codex..."
178
+ mkdir -p "$CODEX_SKILLS_DIR"
179
+ skill_installed=0
180
+ skill_skipped=0
181
+ for skill_dir in "$SKILLS_DIR"/*/; do
182
+ [ -d "$skill_dir" ] || continue
183
+ skill_file="$skill_dir/SKILL.md"
184
+ [ -f "$skill_file" ] || continue
185
+ skill_name="$(basename "$skill_dir")"
186
+ dest_dir="$CODEX_SKILLS_DIR/$AGENT_NAME-$skill_name"
187
+ dest_file="$dest_dir/SKILL.md"
188
+ if [ -f "$dest_file" ]; then
189
+ skill_skipped=$((skill_skipped + 1))
190
+ else
191
+ mkdir -p "$dest_dir"
192
+ cp "$skill_file" "$dest_file"
193
+ skill_installed=$((skill_installed + 1))
194
+ fi
195
+ done
196
+ echo "[ok] Codex skills: $skill_installed installed, $skill_skipped already present"
197
+ fi
198
+ `
199
+ : '';
200
+
201
+ const nextSteps = [
202
+ 'echo ""',
203
+ 'echo "=== Setup Complete ==="',
204
+ 'echo ""',
205
+ 'echo "Next:"',
206
+ ...(claudeSetup
207
+ ? ['echo " - Start a new Claude Code session (or restart if one is open)"']
208
+ : []),
209
+ ...(codexSetup ? ['echo " - Start a new Codex session (or restart if one is open)"'] : []),
210
+ `echo " - Say: \\"Hello, ${config.name}!\\""`,
211
+ 'echo ""',
212
+ `echo "${config.name} is ready."`,
213
+ ].join('\n');
214
+
215
+ return `#!/usr/bin/env bash
216
+ set -euo pipefail
217
+
218
+ AGENT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
219
+ AGENT_NAME="${config.id}"
220
+
221
+ echo "=== ${config.name} Setup (${hostLabel}) ==="
163
222
  echo ""
164
- echo "${config.name} will activate and guide you from there."
223
+
224
+ # Check Node.js
225
+ if ! command -v node &>/dev/null; then
226
+ echo "Error: Node.js is not installed. Install Node.js 18+ first."
227
+ exit 1
228
+ fi
229
+
230
+ NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
231
+ if [ "$NODE_VERSION" -lt 18 ]; then
232
+ echo "Error: Node.js 18+ required (found v$(node -v))."
233
+ exit 1
234
+ fi
235
+ echo "[ok] Node.js $(node -v)"
236
+
237
+ # Check if built
238
+ if [ ! -f "$AGENT_DIR/dist/index.js" ]; then
239
+ echo ""
240
+ echo "Building ${config.name}..."
241
+ cd "$AGENT_DIR"
242
+ npm install
243
+ npm run build
244
+ echo "[ok] Built successfully"
245
+ else
246
+ echo "[ok] Already built"
247
+ fi
248
+ ${claudeSection}
249
+ ${hookPackSection}
250
+ ${codexSection}
251
+ ${nextSteps}
165
252
  `;
166
253
  }
@@ -68,26 +68,3 @@ export function generateSkills(config: AgentConfig): Array<[string, string]> {
68
68
 
69
69
  return files;
70
70
  }
71
-
72
- /**
73
- * List all bundled skill names with their descriptions (from YAML frontmatter).
74
- */
75
- export function listSkillDescriptions(): Array<{ name: string; description: string }> {
76
- let skillFiles: string[];
77
-
78
- try {
79
- skillFiles = readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md'));
80
- } catch {
81
- return [];
82
- }
83
-
84
- return skillFiles.map((file) => {
85
- const content = readFileSync(join(SKILLS_DIR, file), 'utf-8');
86
- const nameMatch = content.match(/^name:\s*(.+)$/m);
87
- const descMatch = content.match(/^description:\s*"?(.+?)"?\s*$/m);
88
- return {
89
- name: nameMatch?.[1]?.trim() ?? file.replace('.md', ''),
90
- description: descMatch?.[1]?.trim() ?? '',
91
- };
92
- });
93
- }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Template: Telegram agent loop — wires @soleri/core agent loop to MCP tools.
3
+ * Generated by @soleri/forge for agents with Telegram transport.
4
+ */
5
+
6
+ import type { AgentConfig } from '../types.js';
7
+
8
+ export function generateTelegramAgent(config: AgentConfig): string {
9
+ return `/**
10
+ * ${config.name} — Telegram Agent Loop.
11
+ * Generated by @soleri/forge.
12
+ *
13
+ * Wires the @soleri/core agent loop to the agent's MCP tools.
14
+ */
15
+
16
+ import {
17
+ runAgentLoop,
18
+ McpToolBridge,
19
+ createOutputCompressor,
20
+ } from '@soleri/core';
21
+ import type {
22
+ ChatMessage,
23
+ AgentLoopResult,
24
+ AgentCallbacks,
25
+ McpToolRegistration,
26
+ } from '@soleri/core';
27
+ import type { TelegramConfig } from './telegram-config.js';
28
+ import { execFileSync } from 'node:child_process';
29
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
30
+ import { dirname } from 'node:path';
31
+
32
+ // ─── Core Tools ──────────────────────────────────────────────────────
33
+
34
+ const CORE_TOOLS: McpToolRegistration[] = [
35
+ {
36
+ name: 'bash',
37
+ description: 'Execute a shell command. Returns stdout/stderr.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ command: { type: 'string', description: 'Shell command to execute.' },
42
+ timeout: { type: 'number', description: 'Timeout in ms. Default: 120000.' },
43
+ },
44
+ required: ['command'],
45
+ },
46
+ handler: async (input) => {
47
+ try {
48
+ const timeout = (input.timeout as number) ?? 120_000;
49
+ const output = execFileSync('/bin/sh', ['-c', input.command as string], {
50
+ timeout,
51
+ encoding: 'utf-8',
52
+ maxBuffer: 1024 * 1024,
53
+ });
54
+ return output;
55
+ } catch (err: unknown) {
56
+ const error = err as { stdout?: string; stderr?: string; message?: string };
57
+ return \`Error: \${error.stderr ?? error.stdout ?? error.message ?? 'Unknown error'}\`;
58
+ }
59
+ },
60
+ },
61
+ {
62
+ name: 'read_file',
63
+ description: 'Read a file from the filesystem.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ path: { type: 'string', description: 'File path to read.' },
68
+ },
69
+ required: ['path'],
70
+ },
71
+ handler: async (input) => {
72
+ return readFileSync(input.path as string, 'utf-8');
73
+ },
74
+ },
75
+ {
76
+ name: 'write_file',
77
+ description: 'Write content to a file.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ path: { type: 'string', description: 'File path to write.' },
82
+ content: { type: 'string', description: 'Content to write.' },
83
+ },
84
+ required: ['path', 'content'],
85
+ },
86
+ handler: async (input) => {
87
+ mkdirSync(dirname(input.path as string), { recursive: true });
88
+ writeFileSync(input.path as string, input.content as string, 'utf-8');
89
+ return \`Wrote \${(input.content as string).length} bytes to \${input.path}\`;
90
+ },
91
+ },
92
+ {
93
+ name: 'glob',
94
+ description: 'Find files matching a glob pattern.',
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ pattern: { type: 'string', description: 'Glob pattern.' },
99
+ cwd: { type: 'string', description: 'Working directory.' },
100
+ },
101
+ required: ['pattern'],
102
+ },
103
+ handler: async (input) => {
104
+ const cwd = (input.cwd as string) ?? process.cwd();
105
+ const output = execFileSync('find', ['.', '-path', input.pattern as string, '-type', 'f'], {
106
+ cwd,
107
+ encoding: 'utf-8',
108
+ timeout: 10_000,
109
+ });
110
+ const lines = output.trim().split('\\n').filter(Boolean).slice(0, 100);
111
+ return lines.length > 0 ? lines.join('\\n') : 'No files found.';
112
+ },
113
+ },
114
+ {
115
+ name: 'grep',
116
+ description: 'Search file contents with regex.',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {
120
+ pattern: { type: 'string', description: 'Regex pattern.' },
121
+ path: { type: 'string', description: 'File or directory to search.' },
122
+ type: { type: 'string', description: 'File type filter (e.g., ts, js).' },
123
+ },
124
+ required: ['pattern'],
125
+ },
126
+ handler: async (input) => {
127
+ const searchPath = (input.path as string) ?? '.';
128
+ const args = ['--no-heading', '-n', input.pattern as string, searchPath];
129
+ if (input.type) args.splice(2, 0, '--type', input.type as string);
130
+ try {
131
+ const output = execFileSync('rg', args, {
132
+ encoding: 'utf-8',
133
+ timeout: 10_000,
134
+ });
135
+ const lines = output.trim().split('\\n').filter(Boolean).slice(0, 50);
136
+ return lines.length > 0 ? lines.join('\\n') : 'No matches found.';
137
+ } catch {
138
+ return 'No matches found.';
139
+ }
140
+ },
141
+ },
142
+ ];
143
+
144
+ // ─── Agent Factory ───────────────────────────────────────────────────
145
+
146
+ export interface TelegramAgentRunner {
147
+ run(messages: ChatMessage[], callbacks?: AgentCallbacks): Promise<AgentLoopResult>;
148
+ registerTool(tool: McpToolRegistration): void;
149
+ getToolCount(): number;
150
+ }
151
+
152
+ export function createTelegramAgent(config: TelegramConfig): TelegramAgentRunner {
153
+ const bridge = new McpToolBridge({
154
+ compressor: createOutputCompressor({ maxLength: 4000 }),
155
+ maxOutput: 10_000,
156
+ });
157
+
158
+ // Register core tools
159
+ bridge.registerAll(CORE_TOOLS);
160
+
161
+ const systemPrompt = buildSystemPrompt(config);
162
+
163
+ return {
164
+ run: async (messages: ChatMessage[], callbacks?: AgentCallbacks) => {
165
+ return runAgentLoop(messages, {
166
+ apiKey: config.apiKey,
167
+ model: config.model,
168
+ systemPrompt,
169
+ tools: bridge.getTools(),
170
+ executor: bridge.createExecutor(),
171
+ maxIterations: config.maxIterations,
172
+ maxTokens: config.maxTokens,
173
+ }, callbacks);
174
+ },
175
+
176
+ registerTool: (tool: McpToolRegistration) => {
177
+ bridge.register(tool);
178
+ },
179
+
180
+ getToolCount: () => bridge.size,
181
+ };
182
+ }
183
+
184
+ // ─── System Prompt ───────────────────────────────────────────────────
185
+
186
+ function buildSystemPrompt(config: TelegramConfig): string {
187
+ const parts: string[] = [
188
+ '# ${config.name}',
189
+ '',
190
+ '${config.role}',
191
+ '',
192
+ '## Environment',
193
+ \`- Platform: \${process.platform}\`,
194
+ \`- Working directory: \${config.workingDirectory ?? process.cwd()}\`,
195
+ \`- Date: \${new Date().toISOString().split('T')[0]}\`,
196
+ '',
197
+ '## Telegram Formatting',
198
+ '- Keep responses concise (under 2000 chars when possible)',
199
+ '- Use Markdown: **bold**, *italic*, \\\`code\\\`, \\\`\\\`\\\`code blocks\\\`\\\`\\\`',
200
+ '- Use bullet points for lists',
201
+ '- No raw JSON in responses — format for human reading',
202
+ '',
203
+ '## Available Tools',
204
+ '- bash: Execute shell commands',
205
+ '- read_file: Read file contents',
206
+ '- write_file: Write to files',
207
+ '- glob: Find files by pattern',
208
+ '- grep: Search file contents',
209
+ ];
210
+
211
+ return parts.join('\\n');
212
+ }
213
+ `;
214
+ }