@@ -1677,6 +1677,225 @@ int cbm_remove_indexes(const char *home_dir) {
16771677 return count ;
16781678}
16791679
1680+ /* ── Config store (persistent key-value in _config.db) ─────────── */
1681+
1682+ #include <sqlite3.h>
1683+
1684+ struct cbm_config {
1685+ sqlite3 * db ;
1686+ char get_buf [4096 ]; /* static buffer for cbm_config_get return values */
1687+ };
1688+
1689+ cbm_config_t * cbm_config_open (const char * cache_dir ) {
1690+ if (!cache_dir ) {
1691+ return NULL ;
1692+ }
1693+
1694+ char dbpath [1024 ];
1695+ snprintf (dbpath , sizeof (dbpath ), "%s/_config.db" , cache_dir );
1696+
1697+ /* Ensure directory exists */
1698+ mkdirp (cache_dir , DIR_PERMS );
1699+
1700+ sqlite3 * db = NULL ;
1701+ if (sqlite3_open (dbpath , & db ) != SQLITE_OK ) {
1702+ if (db ) {
1703+ sqlite3_close (db );
1704+ }
1705+ return NULL ;
1706+ }
1707+
1708+ /* Create table if not exists */
1709+ const char * sql = "CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)" ;
1710+ char * err_msg = NULL ;
1711+ if (sqlite3_exec (db , sql , NULL , NULL , & err_msg ) != SQLITE_OK ) {
1712+ sqlite3_free (err_msg );
1713+ sqlite3_close (db );
1714+ return NULL ;
1715+ }
1716+
1717+ cbm_config_t * cfg = calloc (1 , sizeof (* cfg ));
1718+ if (!cfg ) {
1719+ sqlite3_close (db );
1720+ return NULL ;
1721+ }
1722+ cfg -> db = db ;
1723+ return cfg ;
1724+ }
1725+
1726+ void cbm_config_close (cbm_config_t * cfg ) {
1727+ if (!cfg ) {
1728+ return ;
1729+ }
1730+ if (cfg -> db ) {
1731+ sqlite3_close (cfg -> db );
1732+ }
1733+ free (cfg );
1734+ }
1735+
1736+ const char * cbm_config_get (cbm_config_t * cfg , const char * key , const char * default_val ) {
1737+ if (!cfg || !key ) {
1738+ return default_val ;
1739+ }
1740+
1741+ sqlite3_stmt * stmt = NULL ;
1742+ if (sqlite3_prepare_v2 (cfg -> db , "SELECT value FROM config WHERE key = ?" , -1 , & stmt , NULL ) !=
1743+ SQLITE_OK ) {
1744+ return default_val ;
1745+ }
1746+ sqlite3_bind_text (stmt , 1 , key , -1 , SQLITE_TRANSIENT );
1747+
1748+ const char * result = default_val ;
1749+ if (sqlite3_step (stmt ) == SQLITE_ROW ) {
1750+ const char * val = (const char * )sqlite3_column_text (stmt , 0 );
1751+ if (val ) {
1752+ snprintf (cfg -> get_buf , sizeof (cfg -> get_buf ), "%s" , val );
1753+ result = cfg -> get_buf ;
1754+ }
1755+ }
1756+ sqlite3_finalize (stmt );
1757+ return result ;
1758+ }
1759+
1760+ bool cbm_config_get_bool (cbm_config_t * cfg , const char * key , bool default_val ) {
1761+ const char * val = cbm_config_get (cfg , key , NULL );
1762+ if (!val ) {
1763+ return default_val ;
1764+ }
1765+ if (strcmp (val , "true" ) == 0 || strcmp (val , "1" ) == 0 || strcmp (val , "on" ) == 0 ) {
1766+ return true;
1767+ }
1768+ if (strcmp (val , "false" ) == 0 || strcmp (val , "0" ) == 0 || strcmp (val , "off" ) == 0 ) {
1769+ return false;
1770+ }
1771+ return default_val ;
1772+ }
1773+
1774+ int cbm_config_get_int (cbm_config_t * cfg , const char * key , int default_val ) {
1775+ const char * val = cbm_config_get (cfg , key , NULL );
1776+ if (!val ) {
1777+ return default_val ;
1778+ }
1779+ char * endptr ;
1780+ long v = strtol (val , & endptr , 10 );
1781+ if (endptr == val || * endptr != '\0' ) {
1782+ return default_val ;
1783+ }
1784+ return (int )v ;
1785+ }
1786+
1787+ int cbm_config_set (cbm_config_t * cfg , const char * key , const char * value ) {
1788+ if (!cfg || !key || !value ) {
1789+ return -1 ;
1790+ }
1791+
1792+ sqlite3_stmt * stmt = NULL ;
1793+ if (sqlite3_prepare_v2 (cfg -> db , "INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)" , -1 ,
1794+ & stmt , NULL ) != SQLITE_OK ) {
1795+ return -1 ;
1796+ }
1797+ sqlite3_bind_text (stmt , 1 , key , -1 , SQLITE_TRANSIENT );
1798+ sqlite3_bind_text (stmt , 2 , value , -1 , SQLITE_TRANSIENT );
1799+
1800+ int rc = sqlite3_step (stmt ) == SQLITE_DONE ? 0 : -1 ;
1801+ sqlite3_finalize (stmt );
1802+ return rc ;
1803+ }
1804+
1805+ int cbm_config_delete (cbm_config_t * cfg , const char * key ) {
1806+ if (!cfg || !key ) {
1807+ return -1 ;
1808+ }
1809+
1810+ sqlite3_stmt * stmt = NULL ;
1811+ if (sqlite3_prepare_v2 (cfg -> db , "DELETE FROM config WHERE key = ?" , -1 , & stmt , NULL ) !=
1812+ SQLITE_OK ) {
1813+ return -1 ;
1814+ }
1815+ sqlite3_bind_text (stmt , 1 , key , -1 , SQLITE_TRANSIENT );
1816+
1817+ int rc = sqlite3_step (stmt ) == SQLITE_DONE ? 0 : -1 ;
1818+ sqlite3_finalize (stmt );
1819+ return rc ;
1820+ }
1821+
1822+ /* ── Config CLI subcommand ────────────────────────────────────── */
1823+
1824+ int cbm_cmd_config (int argc , char * * argv ) {
1825+ if (argc == 0 ) {
1826+ printf ("Usage: codebase-memory-mcp config <command> [args]\n\n" );
1827+ printf ("Commands:\n" );
1828+ printf (" list Show all config values\n" );
1829+ printf (" get <key> Get a config value\n" );
1830+ printf (" set <key> <val> Set a config value\n" );
1831+ printf (" reset <key> Reset a key to default\n\n" );
1832+ printf ("Config keys:\n" );
1833+ printf (" %-25s default=%-10s %s\n" , CBM_CONFIG_AUTO_INDEX , "false" ,
1834+ "Enable auto-indexing on MCP session start" );
1835+ printf (" %-25s default=%-10s %s\n" , CBM_CONFIG_AUTO_INDEX_LIMIT , "50000" ,
1836+ "Max files for auto-indexing new projects" );
1837+ return 0 ;
1838+ }
1839+
1840+ // NOLINTNEXTLINE(concurrency-mt-unsafe)
1841+ const char * home = getenv ("HOME" );
1842+ if (!home ) {
1843+ fprintf (stderr , "error: HOME not set\n" );
1844+ return 1 ;
1845+ }
1846+
1847+ char cache_dir [1024 ];
1848+ snprintf (cache_dir , sizeof (cache_dir ), "%s/.cache/codebase-memory-mcp" , home );
1849+
1850+ cbm_config_t * cfg = cbm_config_open (cache_dir );
1851+ if (!cfg ) {
1852+ fprintf (stderr , "error: cannot open config database\n" );
1853+ return 1 ;
1854+ }
1855+
1856+ int rc = 0 ;
1857+ if (strcmp (argv [0 ], "list" ) == 0 || strcmp (argv [0 ], "ls" ) == 0 ) {
1858+ printf ("Configuration:\n" );
1859+ printf (" %-25s = %-10s\n" , CBM_CONFIG_AUTO_INDEX ,
1860+ cbm_config_get (cfg , CBM_CONFIG_AUTO_INDEX , "false" ));
1861+ printf (" %-25s = %-10s\n" , CBM_CONFIG_AUTO_INDEX_LIMIT ,
1862+ cbm_config_get (cfg , CBM_CONFIG_AUTO_INDEX_LIMIT , "50000" ));
1863+ } else if (strcmp (argv [0 ], "get" ) == 0 ) {
1864+ if (argc < 2 ) {
1865+ fprintf (stderr , "Usage: config get <key>\n" );
1866+ rc = 1 ;
1867+ } else {
1868+ printf ("%s\n" , cbm_config_get (cfg , argv [1 ], "" ));
1869+ }
1870+ } else if (strcmp (argv [0 ], "set" ) == 0 ) {
1871+ if (argc < 3 ) {
1872+ fprintf (stderr , "Usage: config set <key> <value>\n" );
1873+ rc = 1 ;
1874+ } else {
1875+ if (cbm_config_set (cfg , argv [1 ], argv [2 ]) == 0 ) {
1876+ printf ("%s = %s\n" , argv [1 ], argv [2 ]);
1877+ } else {
1878+ fprintf (stderr , "error: failed to set %s\n" , argv [1 ]);
1879+ rc = 1 ;
1880+ }
1881+ }
1882+ } else if (strcmp (argv [0 ], "reset" ) == 0 ) {
1883+ if (argc < 2 ) {
1884+ fprintf (stderr , "Usage: config reset <key>\n" );
1885+ rc = 1 ;
1886+ } else {
1887+ cbm_config_delete (cfg , argv [1 ]);
1888+ printf ("%s reset to default\n" , argv [1 ]);
1889+ }
1890+ } else {
1891+ fprintf (stderr , "Unknown config command: %s\n" , argv [0 ]);
1892+ rc = 1 ;
1893+ }
1894+
1895+ cbm_config_close (cfg );
1896+ return rc ;
1897+ }
1898+
16801899/* ── Interactive prompt ───────────────────────────────────────── */
16811900
16821901/* Global auto-answer mode: 0=interactive, 1=always yes, -1=always no */
0 commit comments