@blockrun/franklin 3.8.44 → 3.9.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/README.md +3 -2
- package/dist/agent/commands.d.ts +5 -0
- package/dist/agent/commands.js +28 -0
- package/dist/agent/compact.js +1 -1
- package/dist/agent/context.js +1 -0
- package/dist/agent/llm.js +4 -4
- package/dist/agent/loop.js +40 -13
- package/dist/agent/verification.js +2 -2
- package/dist/commands/balance-retry.d.ts +15 -0
- package/dist/commands/balance-retry.js +20 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +93 -0
- package/dist/commands/social.js +1 -1
- package/dist/commands/start.js +17 -13
- package/dist/commands/telegram.js +1 -1
- package/dist/index.js +9 -0
- package/dist/learnings/extractor.js +3 -3
- package/dist/plugin-sdk/workflow.js +2 -2
- package/dist/pricing.js +1 -1
- package/dist/proxy/fallback.js +1 -1
- package/dist/proxy/server.js +10 -10
- package/dist/router/index.js +8 -8
- package/dist/skills/bootstrap.d.ts +27 -0
- package/dist/skills/bootstrap.js +40 -0
- package/dist/skills/invoke.d.ts +23 -0
- package/dist/skills/invoke.js +38 -0
- package/dist/skills/loader.d.ts +21 -0
- package/dist/skills/loader.js +149 -0
- package/dist/skills/registry.d.ts +26 -0
- package/dist/skills/registry.js +54 -0
- package/dist/skills/types.d.ts +47 -0
- package/dist/skills/types.js +8 -0
- package/dist/skills-bundled/budget-grill/SKILL.md +24 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/moa.js +4 -4
- package/dist/tools/subagent.js +3 -3
- package/dist/tools/tool-categories.js +3 -0
- package/dist/tools/wallet.d.ts +23 -0
- package/dist/tools/wallet.js +63 -0
- package/dist/ui/app.js +3 -3
- package/dist/ui/model-picker.js +13 -17
- package/package.json +4 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-time helpers that wire the skills library into the running process:
|
|
3
|
+
*
|
|
4
|
+
* - `loadBundledSkills()` discovers `dist/skills-bundled/<name>/SKILL.md`
|
|
5
|
+
* relative to this module's location and returns a populated Registry.
|
|
6
|
+
* User-global and project-local discovery are deferred to Phase 2 of the
|
|
7
|
+
* skills MVP plan; today we only ship the bundled set.
|
|
8
|
+
*
|
|
9
|
+
* - `getSkillVars()` returns the synchronously-known runtime variables
|
|
10
|
+
* that `substituteVariables` injects into a skill body before
|
|
11
|
+
* `$ARGUMENTS` expansion. Async values (wallet balance, on-chain reads)
|
|
12
|
+
* are deferred to a later phase: those vars stay literal in the rendered
|
|
13
|
+
* prompt and `substituteVariables` leaves unknown vars intact.
|
|
14
|
+
*/
|
|
15
|
+
import { Registry } from './registry.js';
|
|
16
|
+
import type { LoadError } from './types.js';
|
|
17
|
+
export interface BundledLoad {
|
|
18
|
+
registry: Registry;
|
|
19
|
+
errors: LoadError[];
|
|
20
|
+
}
|
|
21
|
+
export declare function loadBundledSkills(): BundledLoad;
|
|
22
|
+
export interface SkillVarSource {
|
|
23
|
+
chain?: 'base' | 'solana';
|
|
24
|
+
/** Per-turn spend cap in USD; mirrors the `max-turn-spend-usd` config key. */
|
|
25
|
+
perTurnCapUsd?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function getSkillVars(src: SkillVarSource): Record<string, string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-time helpers that wire the skills library into the running process:
|
|
3
|
+
*
|
|
4
|
+
* - `loadBundledSkills()` discovers `dist/skills-bundled/<name>/SKILL.md`
|
|
5
|
+
* relative to this module's location and returns a populated Registry.
|
|
6
|
+
* User-global and project-local discovery are deferred to Phase 2 of the
|
|
7
|
+
* skills MVP plan; today we only ship the bundled set.
|
|
8
|
+
*
|
|
9
|
+
* - `getSkillVars()` returns the synchronously-known runtime variables
|
|
10
|
+
* that `substituteVariables` injects into a skill body before
|
|
11
|
+
* `$ARGUMENTS` expansion. Async values (wallet balance, on-chain reads)
|
|
12
|
+
* are deferred to a later phase: those vars stay literal in the rendered
|
|
13
|
+
* prompt and `substituteVariables` leaves unknown vars intact.
|
|
14
|
+
*/
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { loadSkillsFromDir } from './loader.js';
|
|
18
|
+
import { Registry } from './registry.js';
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
// Built form lives at dist/skills/bootstrap.js, so dist/skills-bundled/
|
|
21
|
+
// is one level up + sibling.
|
|
22
|
+
const BUNDLED_DIR = join(HERE, '..', 'skills-bundled');
|
|
23
|
+
export function loadBundledSkills() {
|
|
24
|
+
const result = loadSkillsFromDir(BUNDLED_DIR, 'bundled');
|
|
25
|
+
return { registry: Registry.fromLoaded(result.skills), errors: result.errors };
|
|
26
|
+
}
|
|
27
|
+
export function getSkillVars(src) {
|
|
28
|
+
const out = {};
|
|
29
|
+
if (src.chain)
|
|
30
|
+
out.wallet_chain = src.chain;
|
|
31
|
+
if (typeof src.perTurnCapUsd === 'number' && Number.isFinite(src.perTurnCapUsd)) {
|
|
32
|
+
const cap = src.perTurnCapUsd.toFixed(2);
|
|
33
|
+
out.per_turn_cap = cap;
|
|
34
|
+
// Skills run at turn boundary, so spent-this-turn is always zero at
|
|
35
|
+
// substitution time and remaining equals the cap.
|
|
36
|
+
out.spent_this_turn = '0.00';
|
|
37
|
+
out.turn_budget_remaining = cap;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill invocation helpers.
|
|
3
|
+
*
|
|
4
|
+
* `substituteVariables` is the only piece of the invocation path that runs
|
|
5
|
+
* for every skill: it inlines `{{wallet_balance}}` and similar runtime
|
|
6
|
+
* context, then expands `$ARGUMENTS` to the trailing slash-command argument.
|
|
7
|
+
*
|
|
8
|
+
* Both substitutions use function-form replacement so that values containing
|
|
9
|
+
* `$` or other replacement-pattern meta-characters (like a user task that
|
|
10
|
+
* mentions "find $5 of value") are inserted verbatim.
|
|
11
|
+
*/
|
|
12
|
+
export declare function substituteVariables(body: string, vars: Record<string, string>, args: string): string;
|
|
13
|
+
import type { Registry } from './registry.js';
|
|
14
|
+
export interface SkillMatch {
|
|
15
|
+
rewritten: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pure dispatch: given a slash-command input line, look up the skill in the
|
|
19
|
+
* registry, render its body against runtime variables and arguments, and
|
|
20
|
+
* return the prompt the agent should run. Returns null when the input is
|
|
21
|
+
* not a slash command, the slash is bare, or no skill of that name exists.
|
|
22
|
+
*/
|
|
23
|
+
export declare function matchSkill(input: string, registry: Registry, vars: Record<string, string>): SkillMatch | null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill invocation helpers.
|
|
3
|
+
*
|
|
4
|
+
* `substituteVariables` is the only piece of the invocation path that runs
|
|
5
|
+
* for every skill: it inlines `{{wallet_balance}}` and similar runtime
|
|
6
|
+
* context, then expands `$ARGUMENTS` to the trailing slash-command argument.
|
|
7
|
+
*
|
|
8
|
+
* Both substitutions use function-form replacement so that values containing
|
|
9
|
+
* `$` or other replacement-pattern meta-characters (like a user task that
|
|
10
|
+
* mentions "find $5 of value") are inserted verbatim.
|
|
11
|
+
*/
|
|
12
|
+
const VAR_PATTERN = /\{\{(\w+)\}\}/g;
|
|
13
|
+
export function substituteVariables(body, vars, args) {
|
|
14
|
+
const withVars = body.replace(VAR_PATTERN, (match, key) => {
|
|
15
|
+
return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match;
|
|
16
|
+
});
|
|
17
|
+
return withVars.replaceAll('$ARGUMENTS', () => args);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pure dispatch: given a slash-command input line, look up the skill in the
|
|
21
|
+
* registry, render its body against runtime variables and arguments, and
|
|
22
|
+
* return the prompt the agent should run. Returns null when the input is
|
|
23
|
+
* not a slash command, the slash is bare, or no skill of that name exists.
|
|
24
|
+
*/
|
|
25
|
+
export function matchSkill(input, registry, vars) {
|
|
26
|
+
if (!input.startsWith('/'))
|
|
27
|
+
return null;
|
|
28
|
+
const space = input.indexOf(' ');
|
|
29
|
+
const name = (space < 0 ? input : input.slice(0, space)).slice(1);
|
|
30
|
+
if (name.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
const skill = registry.lookup(name);
|
|
33
|
+
if (!skill)
|
|
34
|
+
return null;
|
|
35
|
+
const args = space < 0 ? '' : input.slice(space + 1).trim();
|
|
36
|
+
const rewritten = substituteVariables(skill.skill.body, vars, args);
|
|
37
|
+
return { rewritten };
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKILL.md loader — parses Anthropic-spec frontmatter + markdown body.
|
|
3
|
+
*
|
|
4
|
+
* The frontmatter parser is intentionally minimal: flat `key: value` lines
|
|
5
|
+
* with scalar values (string, boolean, number). Quoted strings, nested
|
|
6
|
+
* objects, and arrays are out of scope for MVP — Anthropic's SKILL.md spec
|
|
7
|
+
* never needs them and adding a YAML dependency would be heavier than the
|
|
8
|
+
* spec warrants.
|
|
9
|
+
*/
|
|
10
|
+
import type { LoadResult, ParseResult, SkillSource } from './types.js';
|
|
11
|
+
export declare function parseSkill(content: string): ParseResult;
|
|
12
|
+
/**
|
|
13
|
+
* Discover and parse every `<dir>/<name>/SKILL.md` under `root`.
|
|
14
|
+
*
|
|
15
|
+
* Missing root → empty result, no error (callers happily union project +
|
|
16
|
+
* user + bundled, and absent dirs are normal).
|
|
17
|
+
*
|
|
18
|
+
* Parse failures on individual skills do not abort the load; they are
|
|
19
|
+
* surfaced via `LoadResult.errors` so the caller can warn the user.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadSkillsFromDir(root: string, source: SkillSource): LoadResult;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKILL.md loader — parses Anthropic-spec frontmatter + markdown body.
|
|
3
|
+
*
|
|
4
|
+
* The frontmatter parser is intentionally minimal: flat `key: value` lines
|
|
5
|
+
* with scalar values (string, boolean, number). Quoted strings, nested
|
|
6
|
+
* objects, and arrays are out of scope for MVP — Anthropic's SKILL.md spec
|
|
7
|
+
* never needs them and adding a YAML dependency would be heavier than the
|
|
8
|
+
* spec warrants.
|
|
9
|
+
*/
|
|
10
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
const FRONTMATTER_FENCE = '---';
|
|
13
|
+
export function parseSkill(content) {
|
|
14
|
+
const fmMatch = extractFrontmatter(content);
|
|
15
|
+
if (!fmMatch) {
|
|
16
|
+
return { error: 'missing frontmatter (file must start with --- fence)' };
|
|
17
|
+
}
|
|
18
|
+
const { frontmatter, body } = fmMatch;
|
|
19
|
+
const parsedFrontmatter = parseFrontmatter(frontmatter);
|
|
20
|
+
if ('error' in parsedFrontmatter)
|
|
21
|
+
return { error: parsedFrontmatter.error };
|
|
22
|
+
const { fields, warnings } = parsedFrontmatter;
|
|
23
|
+
if (typeof fields.name !== 'string' || fields.name.length === 0) {
|
|
24
|
+
return { error: 'frontmatter missing required field: name' };
|
|
25
|
+
}
|
|
26
|
+
if (typeof fields.description !== 'string' || fields.description.length === 0) {
|
|
27
|
+
return { error: 'frontmatter missing required field: description' };
|
|
28
|
+
}
|
|
29
|
+
const skill = {
|
|
30
|
+
name: fields.name,
|
|
31
|
+
description: fields.description,
|
|
32
|
+
body,
|
|
33
|
+
};
|
|
34
|
+
if (typeof fields['argument-hint'] === 'string') {
|
|
35
|
+
skill.argumentHint = fields['argument-hint'];
|
|
36
|
+
}
|
|
37
|
+
if (typeof fields['disable-model-invocation'] === 'boolean') {
|
|
38
|
+
skill.disableModelInvocation = fields['disable-model-invocation'];
|
|
39
|
+
}
|
|
40
|
+
if (typeof fields['budget-cap-usd'] === 'number') {
|
|
41
|
+
skill.budgetCapUsd = fields['budget-cap-usd'];
|
|
42
|
+
}
|
|
43
|
+
if (typeof fields['cost-receipt'] === 'boolean') {
|
|
44
|
+
skill.costReceipt = fields['cost-receipt'];
|
|
45
|
+
}
|
|
46
|
+
return { skill, warnings };
|
|
47
|
+
}
|
|
48
|
+
function extractFrontmatter(content) {
|
|
49
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
50
|
+
if (!normalized.startsWith(FRONTMATTER_FENCE + '\n'))
|
|
51
|
+
return null;
|
|
52
|
+
const rest = normalized.slice(FRONTMATTER_FENCE.length + 1);
|
|
53
|
+
const closeIdx = rest.indexOf('\n' + FRONTMATTER_FENCE + '\n');
|
|
54
|
+
if (closeIdx < 0)
|
|
55
|
+
return null;
|
|
56
|
+
const frontmatter = rest.slice(0, closeIdx);
|
|
57
|
+
const body = rest.slice(closeIdx + 1 + FRONTMATTER_FENCE.length + 1);
|
|
58
|
+
return { frontmatter, body };
|
|
59
|
+
}
|
|
60
|
+
function parseFrontmatter(text) {
|
|
61
|
+
const fields = {};
|
|
62
|
+
const warnings = [];
|
|
63
|
+
const lines = text.split('\n');
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const line = lines[i];
|
|
66
|
+
if (line.trim() === '' || line.trim().startsWith('#'))
|
|
67
|
+
continue;
|
|
68
|
+
const colon = line.indexOf(':');
|
|
69
|
+
if (colon < 0) {
|
|
70
|
+
return { error: `frontmatter line ${i + 1} is not key: value — got "${line}"` };
|
|
71
|
+
}
|
|
72
|
+
const key = line.slice(0, colon).trim();
|
|
73
|
+
const rawValue = line.slice(colon + 1).trim();
|
|
74
|
+
if (key.length === 0) {
|
|
75
|
+
return { error: `frontmatter line ${i + 1} has empty key` };
|
|
76
|
+
}
|
|
77
|
+
fields[key] = parseScalar(rawValue);
|
|
78
|
+
}
|
|
79
|
+
return { fields, warnings };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Discover and parse every `<dir>/<name>/SKILL.md` under `root`.
|
|
83
|
+
*
|
|
84
|
+
* Missing root → empty result, no error (callers happily union project +
|
|
85
|
+
* user + bundled, and absent dirs are normal).
|
|
86
|
+
*
|
|
87
|
+
* Parse failures on individual skills do not abort the load; they are
|
|
88
|
+
* surfaced via `LoadResult.errors` so the caller can warn the user.
|
|
89
|
+
*/
|
|
90
|
+
export function loadSkillsFromDir(root, source) {
|
|
91
|
+
const result = { skills: [], errors: [] };
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = readdirSync(root);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const dirPath = join(root, entry);
|
|
101
|
+
let entryStat;
|
|
102
|
+
try {
|
|
103
|
+
entryStat = statSync(dirPath);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!entryStat.isDirectory())
|
|
109
|
+
continue;
|
|
110
|
+
const skillPath = join(dirPath, 'SKILL.md');
|
|
111
|
+
let content;
|
|
112
|
+
try {
|
|
113
|
+
content = readFileSync(skillPath, 'utf8');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const parsed = parseSkill(content);
|
|
119
|
+
if ('error' in parsed) {
|
|
120
|
+
result.errors.push({ path: skillPath, error: parsed.error });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const warnings = [...parsed.warnings];
|
|
124
|
+
let skill = parsed.skill;
|
|
125
|
+
if (skill.name !== entry) {
|
|
126
|
+
warnings.push(`frontmatter name "${skill.name}" disagrees with directory "${entry}"; using directory name`);
|
|
127
|
+
skill = { ...skill, name: entry };
|
|
128
|
+
}
|
|
129
|
+
const loaded = { skill, source, path: skillPath, warnings };
|
|
130
|
+
result.skills.push(loaded);
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
function parseScalar(raw) {
|
|
135
|
+
if (raw === 'true')
|
|
136
|
+
return true;
|
|
137
|
+
if (raw === 'false')
|
|
138
|
+
return false;
|
|
139
|
+
if (/^-?\d+$/.test(raw))
|
|
140
|
+
return Number.parseInt(raw, 10);
|
|
141
|
+
if (/^-?\d+\.\d+$/.test(raw))
|
|
142
|
+
return Number.parseFloat(raw);
|
|
143
|
+
// Strip surrounding quotes if present.
|
|
144
|
+
if ((raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) ||
|
|
145
|
+
(raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2)) {
|
|
146
|
+
return raw.slice(1, -1);
|
|
147
|
+
}
|
|
148
|
+
return raw;
|
|
149
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill registry — resolves name conflicts across sources and exposes
|
|
3
|
+
* lookup, list, and shadow-set queries for `/help` and
|
|
4
|
+
* `franklin skills list`.
|
|
5
|
+
*
|
|
6
|
+
* Precedence: project > user > bundled. Within the same source, the first
|
|
7
|
+
* loaded skill wins so that the order returned by the loader (which is
|
|
8
|
+
* filesystem-ordered) is the deterministic tiebreaker the user can rely on.
|
|
9
|
+
*
|
|
10
|
+
* Built-in slash commands (e.g. `/security`) take precedence over skills
|
|
11
|
+
* with the same name, but that check happens at the dispatch layer in
|
|
12
|
+
* `src/agent/commands.ts`. The registry never sees the built-in list.
|
|
13
|
+
*/
|
|
14
|
+
import type { LoadedSkill } from './types.js';
|
|
15
|
+
export interface ShadowEntry {
|
|
16
|
+
winner: LoadedSkill;
|
|
17
|
+
loser: LoadedSkill;
|
|
18
|
+
}
|
|
19
|
+
export declare class Registry {
|
|
20
|
+
private readonly byName;
|
|
21
|
+
private readonly shadows;
|
|
22
|
+
static fromLoaded(loaded: LoadedSkill[]): Registry;
|
|
23
|
+
lookup(name: string): LoadedSkill | undefined;
|
|
24
|
+
list(): LoadedSkill[];
|
|
25
|
+
shadowed(): ShadowEntry[];
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill registry — resolves name conflicts across sources and exposes
|
|
3
|
+
* lookup, list, and shadow-set queries for `/help` and
|
|
4
|
+
* `franklin skills list`.
|
|
5
|
+
*
|
|
6
|
+
* Precedence: project > user > bundled. Within the same source, the first
|
|
7
|
+
* loaded skill wins so that the order returned by the loader (which is
|
|
8
|
+
* filesystem-ordered) is the deterministic tiebreaker the user can rely on.
|
|
9
|
+
*
|
|
10
|
+
* Built-in slash commands (e.g. `/security`) take precedence over skills
|
|
11
|
+
* with the same name, but that check happens at the dispatch layer in
|
|
12
|
+
* `src/agent/commands.ts`. The registry never sees the built-in list.
|
|
13
|
+
*/
|
|
14
|
+
const SOURCE_PRIORITY = {
|
|
15
|
+
project: 3,
|
|
16
|
+
user: 2,
|
|
17
|
+
bundled: 1,
|
|
18
|
+
};
|
|
19
|
+
export class Registry {
|
|
20
|
+
byName = new Map();
|
|
21
|
+
shadows = [];
|
|
22
|
+
static fromLoaded(loaded) {
|
|
23
|
+
const reg = new Registry();
|
|
24
|
+
// Stable sort by source priority (desc); ties broken by original order
|
|
25
|
+
// so that within a single source, the first-loaded skill wins.
|
|
26
|
+
const indexed = loaded.map((l, i) => ({ l, i }));
|
|
27
|
+
indexed.sort((a, b) => {
|
|
28
|
+
const pa = SOURCE_PRIORITY[a.l.source];
|
|
29
|
+
const pb = SOURCE_PRIORITY[b.l.source];
|
|
30
|
+
if (pa !== pb)
|
|
31
|
+
return pb - pa;
|
|
32
|
+
return a.i - b.i;
|
|
33
|
+
});
|
|
34
|
+
for (const { l } of indexed) {
|
|
35
|
+
const existing = reg.byName.get(l.skill.name);
|
|
36
|
+
if (!existing) {
|
|
37
|
+
reg.byName.set(l.skill.name, l);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
reg.shadows.push({ winner: existing, loser: l });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return reg;
|
|
44
|
+
}
|
|
45
|
+
lookup(name) {
|
|
46
|
+
return this.byName.get(name);
|
|
47
|
+
}
|
|
48
|
+
list() {
|
|
49
|
+
return [...this.byName.values()].sort((a, b) => a.skill.name.localeCompare(b.skill.name));
|
|
50
|
+
}
|
|
51
|
+
shadowed() {
|
|
52
|
+
return [...this.shadows];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for Franklin's skills layer.
|
|
3
|
+
*
|
|
4
|
+
* A "skill" is an Anthropic-spec SKILL.md file: YAML frontmatter + a markdown
|
|
5
|
+
* body that becomes the prompt-rewrite for a slash command. See
|
|
6
|
+
* docs/plans/2026-04-29-franklin-skills-mvp-design.md.
|
|
7
|
+
*/
|
|
8
|
+
export type SkillSource = 'bundled' | 'user' | 'project';
|
|
9
|
+
export interface ParsedSkill {
|
|
10
|
+
/** Kebab-case skill identifier; matches the parent directory name. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Short description shown in /help and franklin skills list. */
|
|
13
|
+
description: string;
|
|
14
|
+
/** Raw markdown body (not yet variable-substituted). */
|
|
15
|
+
body: string;
|
|
16
|
+
/** Anthropic spec: hint shown after the slash command. */
|
|
17
|
+
argumentHint?: string;
|
|
18
|
+
/** Anthropic spec: when true, the model must not auto-invoke this skill. */
|
|
19
|
+
disableModelInvocation?: boolean;
|
|
20
|
+
/** Franklin extension: hard cap (USD) for the turn this skill kicks off. */
|
|
21
|
+
budgetCapUsd?: number;
|
|
22
|
+
/** Franklin extension: append a paid-call receipt under the agent reply. */
|
|
23
|
+
costReceipt?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export type ParseResult = {
|
|
26
|
+
skill: ParsedSkill;
|
|
27
|
+
warnings: string[];
|
|
28
|
+
} | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
export interface LoadedSkill {
|
|
32
|
+
skill: ParsedSkill;
|
|
33
|
+
source: SkillSource;
|
|
34
|
+
/** Absolute path to the SKILL.md file. */
|
|
35
|
+
path: string;
|
|
36
|
+
/** Non-fatal warnings raised while loading this skill. */
|
|
37
|
+
warnings: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface LoadError {
|
|
40
|
+
/** Absolute path to the file that failed. */
|
|
41
|
+
path: string;
|
|
42
|
+
error: string;
|
|
43
|
+
}
|
|
44
|
+
export interface LoadResult {
|
|
45
|
+
skills: LoadedSkill[];
|
|
46
|
+
errors: LoadError[];
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for Franklin's skills layer.
|
|
3
|
+
*
|
|
4
|
+
* A "skill" is an Anthropic-spec SKILL.md file: YAML frontmatter + a markdown
|
|
5
|
+
* body that becomes the prompt-rewrite for a slash command. See
|
|
6
|
+
* docs/plans/2026-04-29-franklin-skills-mvp-design.md.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: budget-grill
|
|
3
|
+
description: Wallet-aware grilling — interview me about a plan one question at a time, with each branch of the decision tree framed as a USDC cost impact
|
|
4
|
+
argument-hint: <plan or topic to grill on>
|
|
5
|
+
cost-receipt: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are running inside Franklin, an Economic Agent powered by an x402 USDC wallet on {{wallet_chain}}. The user has set a per-turn spend cap of {{per_turn_cap}} USDC, of which {{spent_this_turn}} has been spent so far ({{turn_budget_remaining}} remaining).
|
|
9
|
+
|
|
10
|
+
Your job: interview the user relentlessly about the plan below, **one question at a time**, until you reach a shared understanding of every branch of the decision tree. For every question, also propose your recommended answer and the reasoning behind it.
|
|
11
|
+
|
|
12
|
+
The thing that makes this skill different from a generic grilling session: **frame every option in cost terms**. For each branch, estimate the USDC spend per call/run/cycle, the model tier it would land on, and the worst-case wallet drain over the lifetime of the feature. If the option spends $0 because it's free-tier, say so explicitly. If it depends on a paid tool (`ExaSearch`, `ImageGen`, `VideoGen`, `MusicGen`, `TradingMarket` paid actions), name the tool and estimate the per-call cost.
|
|
13
|
+
|
|
14
|
+
Rules of engagement:
|
|
15
|
+
|
|
16
|
+
1. **One question per response.** Do not stack questions.
|
|
17
|
+
2. **Walk down the decision tree.** Resolve dependencies between decisions one by one — a question that depends on the answer to another comes later.
|
|
18
|
+
3. **Recommend an answer.** Every question carries your recommendation + the cost-impact reasoning behind it.
|
|
19
|
+
4. **Cross-reference the codebase.** If a question can be answered by reading the code, read the code instead of asking. Use `Read`, `Grep`, `Glob`. The user's time is more expensive than tool calls.
|
|
20
|
+
5. **Stop at saturation, not exhaustion.** When the marginal next question stops uncovering new cost trade-offs or design decisions, propose the agreed plan back to the user as a numbered summary, with each step's projected cost and the running total.
|
|
21
|
+
|
|
22
|
+
The plan or topic to grill on:
|
|
23
|
+
|
|
24
|
+
$ARGUMENTS
|
package/dist/tools/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { searchXCapability } from './searchx.js';
|
|
|
23
23
|
import { postToXCapability } from './posttox.js';
|
|
24
24
|
import { moaCapability } from './moa.js';
|
|
25
25
|
import { webhookPostCapability } from './webhook.js';
|
|
26
|
+
import { walletCapability } from './wallet.js';
|
|
26
27
|
import { createTradingCapabilities } from './trading-execute.js';
|
|
27
28
|
import { Portfolio } from '../trading/portfolio.js';
|
|
28
29
|
import { RiskEngine } from '../trading/risk.js';
|
|
@@ -140,6 +141,7 @@ export const allCapabilities = [
|
|
|
140
141
|
postToXCapability,
|
|
141
142
|
moaCapability,
|
|
142
143
|
webhookPostCapability,
|
|
144
|
+
walletCapability,
|
|
143
145
|
];
|
|
144
146
|
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
|
|
145
147
|
export { createSubAgentCapability } from './subagent.js';
|
package/dist/tools/moa.js
CHANGED
|
@@ -14,14 +14,14 @@ import { ModelClient } from '../agent/llm.js';
|
|
|
14
14
|
// ─── Configuration ────────────────────────────────────────────────────────
|
|
15
15
|
/** Reference models — diverse, cheap/free models for parallel queries. */
|
|
16
16
|
const REFERENCE_MODELS = [
|
|
17
|
-
'nvidia/
|
|
18
|
-
'nvidia/
|
|
19
|
-
'nvidia/
|
|
17
|
+
'nvidia/qwen3-coder-480b', // Free, agent-tested coding
|
|
18
|
+
'nvidia/llama-4-maverick', // Free, agent-tested general chat
|
|
19
|
+
'nvidia/glm-4.7', // Free chat fallback
|
|
20
20
|
'google/gemini-2.5-flash', // Fast, cheap
|
|
21
21
|
'deepseek/deepseek-chat', // Cheap, good reasoning
|
|
22
22
|
];
|
|
23
23
|
/** Aggregator model — free by default. Users explicitly pass `aggregator` to upgrade. */
|
|
24
|
-
const AGGREGATOR_MODEL = 'nvidia/
|
|
24
|
+
const AGGREGATOR_MODEL = 'nvidia/qwen3-coder-480b';
|
|
25
25
|
/** Max tokens per reference response. */
|
|
26
26
|
const REFERENCE_MAX_TOKENS = 4096;
|
|
27
27
|
/** Max tokens for aggregator. */
|
package/dist/tools/subagent.js
CHANGED
|
@@ -18,7 +18,7 @@ async function execute(input, ctx) {
|
|
|
18
18
|
return { output: 'Error: prompt is required', isError: true };
|
|
19
19
|
}
|
|
20
20
|
// Resolve which model the sub-agent will actually run on
|
|
21
|
-
const subModel = model || registeredParentModel || 'nvidia/
|
|
21
|
+
const subModel = model || registeredParentModel || 'nvidia/qwen3-coder-480b';
|
|
22
22
|
// Cost gate: if parent is free but sub-agent wants paid, ask user first.
|
|
23
23
|
// Prevents silent charges when the agent decides to spawn a more capable sub-agent.
|
|
24
24
|
if (isFreeModel(registeredParentModel) && !isFreeModel(subModel)) {
|
|
@@ -27,14 +27,14 @@ async function execute(input, ctx) {
|
|
|
27
27
|
// No way to prompt the user (daemon/panel/non-interactive mode).
|
|
28
28
|
// Fail closed — refuse the paid spawn rather than silently charging.
|
|
29
29
|
return {
|
|
30
|
-
output: `Sub-agent declined: parent is on a free model but sub-agent requested a paid model (${shortLabel}). No interactive prompt available. Retry with model='
|
|
30
|
+
output: `Sub-agent declined: parent is on a free model but sub-agent requested a paid model (${shortLabel}). No interactive prompt available. Retry with model='free' or run interactively to approve.`,
|
|
31
31
|
isError: true,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
const answer = await ctx.onAskUser(`Sub-agent wants to use ${shortLabel} (paid). Approve?`, ['y', 'n']);
|
|
35
35
|
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
36
36
|
return {
|
|
37
|
-
output: `Sub-agent skipped — user declined paid model (${shortLabel}). Retry with a free model like
|
|
37
|
+
output: `Sub-agent skipped — user declined paid model (${shortLabel}). Retry with a free model like free.`,
|
|
38
38
|
isError: true,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
@@ -50,6 +50,9 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
50
50
|
// enough that every model tends to pick it correctly.
|
|
51
51
|
'WebFetch',
|
|
52
52
|
'WebSearch',
|
|
53
|
+
// Wallet read — Franklin is the agent with a wallet, so balance + chain
|
|
54
|
+
// + address must be a one-call answer rather than a Bash shell-out.
|
|
55
|
+
'Wallet',
|
|
53
56
|
]);
|
|
54
57
|
/** True if this tool is always available without activation. */
|
|
55
58
|
export function isCoreTool(name) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet capability — direct read of Franklin's wallet status.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists as a first-class tool: without it, "what's my balance"
|
|
5
|
+
* routes through Bash (`franklin balance`) plus parsing, which costs both
|
|
6
|
+
* extra tool turns and ~1KB of bash-tool framing in the model's input
|
|
7
|
+
* window every turn. With Wallet as a dedicated zero-arg tool, the agent
|
|
8
|
+
* can answer balance questions in one cheap call. It also preserves the
|
|
9
|
+
* Economic-Agent positioning: the wallet is a first-class concept, not a
|
|
10
|
+
* shell command.
|
|
11
|
+
*
|
|
12
|
+
* Bash is still available for non-trivial wallet operations (sending,
|
|
13
|
+
* signing arbitrary tx) — Wallet is read-only by design.
|
|
14
|
+
*/
|
|
15
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
16
|
+
export interface WalletReportInput {
|
|
17
|
+
chain: 'base' | 'solana';
|
|
18
|
+
address: string;
|
|
19
|
+
balanceUsd: number;
|
|
20
|
+
}
|
|
21
|
+
/** Pure formatter — small, deterministic, easy to unit-test. */
|
|
22
|
+
export declare function formatWalletReport(input: WalletReportInput): string;
|
|
23
|
+
export declare const walletCapability: CapabilityHandler;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet capability — direct read of Franklin's wallet status.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists as a first-class tool: without it, "what's my balance"
|
|
5
|
+
* routes through Bash (`franklin balance`) plus parsing, which costs both
|
|
6
|
+
* extra tool turns and ~1KB of bash-tool framing in the model's input
|
|
7
|
+
* window every turn. With Wallet as a dedicated zero-arg tool, the agent
|
|
8
|
+
* can answer balance questions in one cheap call. It also preserves the
|
|
9
|
+
* Economic-Agent positioning: the wallet is a first-class concept, not a
|
|
10
|
+
* shell command.
|
|
11
|
+
*
|
|
12
|
+
* Bash is still available for non-trivial wallet operations (sending,
|
|
13
|
+
* signing arbitrary tx) — Wallet is read-only by design.
|
|
14
|
+
*/
|
|
15
|
+
import { loadChain } from '../config.js';
|
|
16
|
+
/** Pure formatter — small, deterministic, easy to unit-test. */
|
|
17
|
+
export function formatWalletReport(input) {
|
|
18
|
+
return [
|
|
19
|
+
`Chain: ${input.chain}`,
|
|
20
|
+
`Address: ${input.address}`,
|
|
21
|
+
`USDC Balance: $${input.balanceUsd.toFixed(2)}`,
|
|
22
|
+
].join('\n');
|
|
23
|
+
}
|
|
24
|
+
async function execute() {
|
|
25
|
+
const chain = loadChain();
|
|
26
|
+
try {
|
|
27
|
+
if (chain === 'solana') {
|
|
28
|
+
const { setupAgentSolanaWallet } = await import('@blockrun/llm');
|
|
29
|
+
const c = await setupAgentSolanaWallet({ silent: true });
|
|
30
|
+
const address = await c.getWalletAddress();
|
|
31
|
+
const balance = await c.getBalance();
|
|
32
|
+
return { output: formatWalletReport({ chain, address, balanceUsd: balance }) };
|
|
33
|
+
}
|
|
34
|
+
const { setupAgentWallet } = await import('@blockrun/llm');
|
|
35
|
+
const c = setupAgentWallet({ silent: true });
|
|
36
|
+
const address = c.getWalletAddress();
|
|
37
|
+
const balance = await c.getBalance();
|
|
38
|
+
return { output: formatWalletReport({ chain, address, balanceUsd: balance }) };
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
42
|
+
return {
|
|
43
|
+
output: `Wallet read failed (${msg}). The user may not have run \`franklin setup\` yet, ` +
|
|
44
|
+
`or the chain RPC is temporarily unreachable. Surface this to the user as-is.`,
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const walletCapability = {
|
|
50
|
+
spec: {
|
|
51
|
+
name: 'Wallet',
|
|
52
|
+
description: 'Read Franklin\'s wallet status — chain, address, and USDC balance. ' +
|
|
53
|
+
'Use this for any "what\'s my balance / how much money / 钱包余额 / wallet status" question. ' +
|
|
54
|
+
'Cheaper and more direct than running `franklin balance` via Bash, and never costs USDC.',
|
|
55
|
+
input_schema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {},
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
execute,
|
|
62
|
+
concurrent: true,
|
|
63
|
+
};
|