14 KiB
Plan: Refactor tools.c into Individual Tool Files
Problem
src/tools.c is ~6,123 lines containing 44 execute_* functions, a ~650-line schema builder, shared helpers, and a dispatcher. It is the largest file in the project and difficult to navigate, review, and maintain.
Goal
Split src/tools.c into focused files organized by tool group, with shared helpers in a common module. The public API (tools.h) remains unchanged — no callers need modification.
Target File Structure
src/
├── tools.h # Public API (UNCHANGED)
├── tools_internal.h # Shared internal helpers + per-tool-file function declarations
├── tools_common.c # Shared helper implementations
├── tools_schema.c # tools_build_openai_schema_json()
├── tools_dispatch.c # tools_init, tools_cleanup, tools_execute dispatcher
└── tools/
├── tool_nostr_post.c # nostr_post, nostr_post_readme, nostr_file_md_to_longform_post
├── tool_nostr_query.c # nostr_query
├── tool_nostr_identity.c # nostr_pubkey, nostr_npub, nostr_encode, nostr_decode
├── tool_nostr_social.c # nostr_react, nostr_profile_get, nostr_nip05_lookup
├── tool_nostr_dm.c # nostr_dm_send, nostr_dm_send_nip17, nostr_encrypt, nostr_decrypt
├── tool_nostr_relay.c # nostr_relay_status, nostr_relay_info
├── tool_nostr_list.c # nostr_list_manage, nostr_delete
├── tool_skill.c # skill_create, skill_list, skill_adopt, skill_remove, skill_edit, skill_search
├── tool_task.c # task_list, task_manage
├── tool_local.c # local_shell_exec, local_file_read, local_file_write, local_http_fetch
├── tool_admin.c # admin_identity, nostr_admin_profile, nostr_admin_contacts, nostr_admin_relays, nostr_admin_notes
├── tool_agent.c # agent_identity, agent_version, message_current
├── tool_model.c # model_get, model_set, model_list
└── tool_meta.c # tool_list, trigger_list
Shared Helpers → tools_common.c
These helpers are used by multiple tool groups and must be non-static in tools_common.c, declared in tools_internal.h:
| Helper | Current lines | Used by |
|---|---|---|
json_error() |
27-35 | ALL tools |
repair_json_control_chars() |
37-101 | nostr_post, nostr_post_readme |
find_key_start() |
103-111 | nostr_post loose parser |
skip_ws() |
113-116 | parse_tool_args_json, loose parser |
parse_loose_kind() |
118-135 | nostr_post |
parse_loose_json_string_value() |
137-186 | nostr_post |
parse_loose_nostr_post_args() |
188-212 | nostr_post |
ensure_tags_array() |
214-227 | nostr_post |
has_tag_key() |
229-243 | longform post helpers |
add_string_tag() |
245-255 | nostr_post, delete, react, skill, longform |
remove_tag_key_all() |
257-270 | upsert_string_tag |
upsert_string_tag() |
272-276 | skill_edit |
parse_tool_args_json() |
590-564 | ALL tools |
is_hex_string_len() |
581-588 | nostr tools, skill tools, DM tools |
find_tag_value_string() |
855-872 | skill tools, list_manage, longform |
tag_tuple_equal() |
813-828 | skill tools, list_manage |
tags_contains_tuple() |
830-840 | skill tools, list_manage |
remove_matching_tag_tuples() |
842-853 | skill tools, list_manage |
build_tool_path() |
(used by local tools + readme) | local tools, readme |
Tool-Local Helpers (stay static in their tool file)
| Helper | Destination |
|---|---|
validate_skill_d_tag() |
tool_skill.c |
fetch_adoption_list_tags() |
tool_skill.c |
publish_adoption_list() |
tool_skill.c |
adoption_tags_contains_address() |
tool_skill.c |
extract_skill_summary() |
tool_skill.c |
ci_contains() |
tool_skill.c |
d_tagify_string() |
tool_nostr_post.c |
first_markdown_h1() |
tool_nostr_post.c |
first_markdown_paragraph() |
tool_nostr_post.c |
trim_copy() |
tool_nostr_post.c |
auto_enrich_longform_tags() |
tool_nostr_post.c |
basename_lowercase_dup() |
tool_nostr_post.c |
detect_ca_bundle_path_for_tools() |
tool_local.c |
local_http_fetch_write_cb() |
tool_local.c |
persist_llm_config() |
tool_model.c |
Tool Function Signatures
Each tool file exposes its execute_* functions via tools_internal.h. Two signature patterns exist:
// Pattern A: context-free (only needs args)
char* execute_nostr_post(const char* args_json);
// Pattern B: context-dependent
char* execute_skill_create(tools_context_t* ctx, const char* args_json);
tools_internal.h Structure
#ifndef DIDACTYL_TOOLS_INTERNAL_H
#define DIDACTYL_TOOLS_INTERNAL_H
#include "tools.h"
#include "cjson/cJSON.h"
// ─── Shared helpers (tools_common.c) ───
char* json_error(const char* msg);
cJSON* parse_tool_args_json(const char* args_json);
int is_hex_string_len(const char* s, size_t expected_len);
int add_string_tag(cJSON* tags, const char* key, const char* value);
int remove_tag_key_all(cJSON* tags, const char* key);
int upsert_string_tag(cJSON* tags, const char* key, const char* value);
int has_tag_key(cJSON* tags, const char* key);
cJSON* ensure_tags_array(cJSON** tags_inout);
cJSON* find_tag_value_string(cJSON* tags, const char* key);
int tag_tuple_equal(cJSON* a, cJSON* b);
int tags_contains_tuple(cJSON* tags, cJSON* tuple);
int remove_matching_tag_tuples(cJSON* tags, cJSON* tuple);
char* repair_json_control_chars(const char* in);
cJSON* parse_loose_nostr_post_args(const char* in);
int build_tool_path(tools_context_t* ctx, const char* rel, char* out, size_t out_size);
// ─── Per-tool-file execute functions ───
// tool_nostr_post.c
char* execute_nostr_post(const char* args_json);
char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json);
char* execute_nostr_file_md_to_longform_post(tools_context_t* ctx, const char* args_json);
// tool_nostr_query.c
char* execute_nostr_query(const char* args_json);
// tool_nostr_identity.c
char* execute_nostr_pubkey(tools_context_t* ctx, const char* args_json);
char* execute_nostr_npub(tools_context_t* ctx, const char* args_json);
char* execute_nostr_encode(const char* args_json);
char* execute_nostr_decode(const char* args_json);
// tool_nostr_social.c
char* execute_nostr_react(const char* args_json);
char* execute_nostr_profile_get(const char* args_json);
char* execute_nostr_nip05_lookup(const char* args_json);
// tool_nostr_dm.c
char* execute_nostr_dm_send(const char* args_json);
char* execute_nostr_dm_send_nip17(const char* args_json);
char* execute_nostr_encrypt(tools_context_t* ctx, const char* args_json);
char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json);
// tool_nostr_relay.c
char* execute_nostr_relay_status(const char* args_json);
char* execute_nostr_relay_info(const char* args_json);
// tool_nostr_list.c
char* execute_nostr_list_manage(tools_context_t* ctx, const char* args_json);
char* execute_nostr_delete(const char* args_json);
// tool_skill.c
char* execute_skill_create(tools_context_t* ctx, const char* args_json);
char* execute_skill_list(tools_context_t* ctx, const char* args_json);
char* execute_skill_adopt(tools_context_t* ctx, const char* args_json);
char* execute_skill_remove(tools_context_t* ctx, const char* args_json);
char* execute_skill_edit(tools_context_t* ctx, const char* args_json);
char* execute_skill_search(const char* args_json);
// tool_task.c
char* execute_task_list(tools_context_t* ctx, const char* args_json);
char* execute_task_manage(tools_context_t* ctx, const char* args_json);
// tool_local.c
char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json);
char* execute_local_shell_exec(tools_context_t* ctx, const char* args_json);
char* execute_local_file_read(tools_context_t* ctx, const char* args_json);
char* execute_local_file_write(tools_context_t* ctx, const char* args_json);
// tool_admin.c
char* execute_admin_identity(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_profile(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_contacts(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_relays(tools_context_t* ctx, const char* args_json);
char* execute_nostr_admin_notes(tools_context_t* ctx, const char* args_json);
// tool_agent.c
char* execute_agent_identity(tools_context_t* ctx, const char* args_json);
char* execute_agent_version(const char* args_json);
char* execute_message_current(tools_context_t* ctx, const char* args_json);
// tool_model.c
char* execute_model_get(const char* args_json);
char* execute_model_set(tools_context_t* ctx, const char* args_json);
char* execute_model_list(const char* args_json);
// tool_meta.c
char* execute_tool_list(tools_context_t* ctx, const char* args_json);
char* execute_trigger_list(tools_context_t* ctx, const char* args_json);
#endif
Makefile Changes
Replace the single $(SRC_DIR)/tools.c entry with:
SRCS = \
$(SRC_DIR)/main.c \
$(SRC_DIR)/config.c \
$(SRC_DIR)/context.c \
$(SRC_DIR)/llm.c \
$(SRC_DIR)/nostr_handler.c \
$(SRC_DIR)/agent.c \
$(SRC_DIR)/tools_common.c \
$(SRC_DIR)/tools_schema.c \
$(SRC_DIR)/tools_dispatch.c \
$(SRC_DIR)/tools/tool_nostr_post.c \
$(SRC_DIR)/tools/tool_nostr_query.c \
$(SRC_DIR)/tools/tool_nostr_identity.c \
$(SRC_DIR)/tools/tool_nostr_social.c \
$(SRC_DIR)/tools/tool_nostr_dm.c \
$(SRC_DIR)/tools/tool_nostr_relay.c \
$(SRC_DIR)/tools/tool_nostr_list.c \
$(SRC_DIR)/tools/tool_skill.c \
$(SRC_DIR)/tools/tool_task.c \
$(SRC_DIR)/tools/tool_local.c \
$(SRC_DIR)/tools/tool_admin.c \
$(SRC_DIR)/tools/tool_agent.c \
$(SRC_DIR)/tools/tool_model.c \
$(SRC_DIR)/tools/tool_meta.c \
$(SRC_DIR)/trigger_manager.c \
$(SRC_DIR)/prompt_template.c \
$(SRC_DIR)/http_api.c \
$(SRC_DIR)/mongoose.c \
$(SRC_DIR)/debug.c
Implementation Steps
Phase 1: Create shared infrastructure
- Create
src/tools_internal.hwith shared helper declarations and allexecute_*prototypes - Create
src/tools_common.c— move shared helpers fromtools.c, removestatic, add#include "tools_internal.h" - Create
src/tools_schema.c— movetools_build_openai_schema_json()fromtools.c - Create
src/tools_dispatch.c— movetools_init(),tools_cleanup(),tools_execute()fromtools.c - Update
MakefileSRCS to include new files (keeptools.ctemporarily with remaining functions) - Build and verify — all tests pass, binary runs
Phase 2: Extract tool files one at a time
Each step: move execute_* functions + their local helpers into the new file, remove from tools.c, build, verify.
- Create
src/tools/directory - Extract
src/tools/tool_nostr_post.c—execute_nostr_post,execute_nostr_post_readme,execute_nostr_file_md_to_longform_post+ local helpers:d_tagify_string,first_markdown_h1,first_markdown_paragraph,trim_copy,auto_enrich_longform_tags,basename_lowercase_dup - Extract
src/tools/tool_nostr_query.c—execute_nostr_query - Extract
src/tools/tool_nostr_identity.c—execute_nostr_pubkey,execute_nostr_npub,execute_nostr_encode,execute_nostr_decode - Extract
src/tools/tool_nostr_social.c—execute_nostr_react,execute_nostr_profile_get,execute_nostr_nip05_lookup - Extract
src/tools/tool_nostr_dm.c—execute_nostr_dm_send,execute_nostr_dm_send_nip17,execute_nostr_encrypt,execute_nostr_decrypt - Extract
src/tools/tool_nostr_relay.c—execute_nostr_relay_status,execute_nostr_relay_info - Extract
src/tools/tool_nostr_list.c—execute_nostr_list_manage,execute_nostr_delete - Extract
src/tools/tool_skill.c—execute_skill_create,execute_skill_list,execute_skill_adopt,execute_skill_remove,execute_skill_edit,execute_skill_search+ local helpers:validate_skill_d_tag,ci_contains,fetch_adoption_list_tags,publish_adoption_list,adoption_tags_contains_address,extract_skill_summary - Extract
src/tools/tool_task.c—execute_task_list,execute_task_manage - Extract
src/tools/tool_local.c—execute_local_http_fetch,execute_local_shell_exec,execute_local_file_read,execute_local_file_write+ local helpers:detect_ca_bundle_path_for_tools,local_http_fetch_write_cb,free_string_array_heap - Extract
src/tools/tool_admin.c—execute_admin_identity,execute_nostr_admin_profile,execute_nostr_admin_contacts,execute_nostr_admin_relays,execute_nostr_admin_notes - Extract
src/tools/tool_agent.c—execute_agent_identity,execute_agent_version,execute_message_current - Extract
src/tools/tool_model.c—execute_model_get,execute_model_set,execute_model_list+ local helper:persist_llm_config - Extract
src/tools/tool_meta.c—execute_tool_list,execute_trigger_list
Phase 3: Cleanup
- Delete
src/tools.c(now empty — all code moved) - Remove
src/tools.cfrom Makefile SRCS - Final build + full test run
- Verify binary size is comparable (no accidental code duplication)
- Increment version and push
Validation Criteria
After each extraction step:
make -j4compiles with zero warnings./didactyl --debug 5starts and runs normally- Tool execution via DM or
--test-toolproduces identical results
Risk Mitigation
- Build after every file extraction — catch issues immediately
- No logic changes — purely mechanical code movement
- Keep
tools.hunchanged — zero impact on callers (agent.c,http_api.c,main.c) - Git commit after each phase — easy rollback points