conare 0.3.5 → 0.3.6

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 (2) hide show
  1. package/dist/index.js +415 -77
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -114,7 +114,7 @@ __export(exports_codebase, {
114
114
  import { createHash as createHash2 } from "node:crypto";
115
115
  import { readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync2, existsSync as existsSync5 } from "node:fs";
116
116
  import { join as join6, relative, extname, resolve, basename as basename3 } from "node:path";
117
- import { execSync } from "node:child_process";
117
+ import { execSync as execSync2 } from "node:child_process";
118
118
  function parseGitignore(rootPath) {
119
119
  const patterns = new Set;
120
120
  const gitignorePath = join6(rootPath, ".gitignore");
@@ -180,12 +180,12 @@ function detectProjectName(rootPath) {
180
180
  }
181
181
  function getChangedFiles(rootPath) {
182
182
  try {
183
- const committed = execSync("git diff --name-only HEAD 2>/dev/null || true", {
183
+ const committed = execSync2("git diff --name-only HEAD 2>/dev/null || true", {
184
184
  cwd: rootPath,
185
185
  encoding: "utf-8",
186
186
  stdio: ["pipe", "pipe", "pipe"]
187
187
  }).trim();
188
- const staged = execSync("git diff --name-only --cached 2>/dev/null || true", {
188
+ const staged = execSync2("git diff --name-only --cached 2>/dev/null || true", {
189
189
  cwd: rootPath,
190
190
  encoding: "utf-8",
191
191
  stdio: ["pipe", "pipe", "pipe"]
@@ -436,7 +436,7 @@ __export(exports_api, {
436
436
  ApiError: () => ApiError
437
437
  });
438
438
  async function apiRequest(path, apiKey, init) {
439
- const res = await fetch(`${API_URL}${path}`, {
439
+ const res = await fetch(`${API_URL2}${path}`, {
440
440
  ...init,
441
441
  headers: {
442
442
  Authorization: `Bearer ${apiKey}`,
@@ -593,7 +593,7 @@ async function uploadBulk(apiKey, memories, onProgress) {
593
593
  }
594
594
  return { success, failed, results };
595
595
  }
596
- var API_URL = "https://mcp.conare.ai", ApiError, PAGE_SIZE = 200, DELETE_CONCURRENCY = 5;
596
+ var API_URL2 = "https://mcp.conare.ai", ApiError, PAGE_SIZE = 200, DELETE_CONCURRENCY = 5;
597
597
  var init_api = __esm(() => {
598
598
  ApiError = class ApiError extends Error {
599
599
  statusCode;
@@ -1425,6 +1425,7 @@ __export(exports_interactive, {
1425
1425
  showDetectedApps: () => showDetectedApps,
1426
1426
  selectMcpTargets: () => selectMcpTargets,
1427
1427
  selectChatSources: () => selectChatSources,
1428
+ promptAuth: () => promptAuth,
1428
1429
  promptApiKey: () => promptApiKey,
1429
1430
  finishSetup: () => finishSetup,
1430
1431
  confirmIndexCodebase: () => confirmIndexCodebase,
@@ -1470,6 +1471,54 @@ async function promptApiKey(options) {
1470
1471
  });
1471
1472
  return ensureValue(keyPrompt).trim() || options.savedApiKey;
1472
1473
  }
1474
+ async function promptAuth(options) {
1475
+ if (options.providedApiKey) {
1476
+ return options.providedApiKey;
1477
+ }
1478
+ if (options.savedApiKey) {
1479
+ const useSaved = await ye({
1480
+ message: `Use saved API key ending in ${options.savedApiKey.slice(-6)}?`,
1481
+ initialValue: true
1482
+ });
1483
+ if (pD(useSaved)) {
1484
+ xe("Setup cancelled.");
1485
+ process.exit(0);
1486
+ }
1487
+ if (useSaved)
1488
+ return options.savedApiKey;
1489
+ }
1490
+ const method = await ve({
1491
+ message: "How would you like to authenticate?",
1492
+ options: [
1493
+ { value: "browser", label: "Sign in with browser", hint: "recommended" },
1494
+ { value: "manual", label: "Enter API key manually" }
1495
+ ]
1496
+ });
1497
+ if (pD(method)) {
1498
+ xe("Setup cancelled.");
1499
+ process.exit(0);
1500
+ }
1501
+ if (method === "browser") {
1502
+ return "__BROWSER_AUTH__";
1503
+ }
1504
+ const keyPrompt = await ge({
1505
+ message: "API key",
1506
+ mask: "*",
1507
+ validate(value) {
1508
+ const resolved = value.trim();
1509
+ if (!resolved)
1510
+ return "Enter an API key from https://mcp.conare.ai";
1511
+ if (!resolved.startsWith("cmem_"))
1512
+ return "API keys start with cmem_";
1513
+ return;
1514
+ }
1515
+ });
1516
+ if (pD(keyPrompt)) {
1517
+ xe("Setup cancelled.");
1518
+ process.exit(0);
1519
+ }
1520
+ return keyPrompt.trim();
1521
+ }
1473
1522
  async function selectChatSources(targets) {
1474
1523
  return ensureValue(await fe({
1475
1524
  message: "Select chat sources",
@@ -1630,26 +1679,98 @@ async function detect() {
1630
1679
  path: cursorDbPath,
1631
1680
  sessionCount: existsSync(cursorDbPath) ? await countCursorSessions(cursorDbPath) : 0
1632
1681
  });
1633
- const openclawDir = join(home, ".openclaw");
1634
- tools.push({
1635
- name: "OpenClaw",
1636
- available: existsSync(openclawDir),
1637
- path: openclawDir,
1638
- sessionCount: 0
1639
- });
1640
1682
  return tools;
1641
1683
  }
1642
1684
 
1685
+ // src/auth.ts
1686
+ import { execSync } from "node:child_process";
1687
+ import { platform as platform2 } from "node:os";
1688
+ var API_URL = "https://mcp.conare.ai";
1689
+ async function browserAuth() {
1690
+ const stateBytes = new Uint8Array(16);
1691
+ crypto.getRandomValues(stateBytes);
1692
+ const state = Array.from(stateBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1693
+ const sessionRes = await fetch(`${API_URL}/api/auth/cli-session`, {
1694
+ method: "POST",
1695
+ headers: { "Content-Type": "application/json" },
1696
+ body: JSON.stringify({ state })
1697
+ });
1698
+ if (!sessionRes.ok) {
1699
+ throw new Error(`Failed to create auth session: HTTP ${sessionRes.status}`);
1700
+ }
1701
+ const { code, expiresAt } = await sessionRes.json();
1702
+ const authUrl = `${API_URL}/cli-auth?code=${code}&state=${state}`;
1703
+ const opened = openBrowser(authUrl);
1704
+ if (!opened) {
1705
+ console.log("");
1706
+ console.log(" Open this URL in your browser to sign in:");
1707
+ console.log("");
1708
+ console.log(` ${authUrl}`);
1709
+ console.log("");
1710
+ }
1711
+ const timeout = Math.max(expiresAt - Date.now(), 0);
1712
+ const deadline = Date.now() + timeout;
1713
+ while (Date.now() < deadline) {
1714
+ await sleep(2000);
1715
+ const exchangeRes = await fetch(`${API_URL}/api/auth/cli-exchange`, {
1716
+ method: "POST",
1717
+ headers: { "Content-Type": "application/json" },
1718
+ body: JSON.stringify({ code, state })
1719
+ });
1720
+ if (exchangeRes.status === 202) {
1721
+ continue;
1722
+ }
1723
+ if (exchangeRes.ok) {
1724
+ const data = await exchangeRes.json();
1725
+ return data.apiKey;
1726
+ }
1727
+ if (exchangeRes.status === 410) {
1728
+ throw new Error("Authentication code was already used. Please try again.");
1729
+ }
1730
+ if (exchangeRes.status === 404) {
1731
+ throw new Error("Authentication session expired. Please try again.");
1732
+ }
1733
+ throw new Error(`Authentication failed: HTTP ${exchangeRes.status}`);
1734
+ }
1735
+ throw new Error("Authentication timed out. Please try again.");
1736
+ }
1737
+ function openBrowser(url) {
1738
+ try {
1739
+ const os = platform2();
1740
+ if (os === "darwin") {
1741
+ execSync(`open "${url}"`, { stdio: "ignore" });
1742
+ } else if (os === "win32") {
1743
+ execSync(`start "" "${url}"`, { stdio: "ignore" });
1744
+ } else {
1745
+ try {
1746
+ execSync(`xdg-open "${url}"`, { stdio: "ignore" });
1747
+ } catch {
1748
+ try {
1749
+ execSync(`wslview "${url}"`, { stdio: "ignore" });
1750
+ } catch {
1751
+ return false;
1752
+ }
1753
+ }
1754
+ }
1755
+ return true;
1756
+ } catch {
1757
+ return false;
1758
+ }
1759
+ }
1760
+ function sleep(ms) {
1761
+ return new Promise((resolve) => setTimeout(resolve, ms));
1762
+ }
1763
+
1643
1764
  // src/ingest/claude.ts
1644
1765
  init_shared();
1645
1766
  import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
1646
1767
  import { join as join3, basename } from "node:path";
1647
- import { homedir as homedir3, platform as platform2 } from "node:os";
1768
+ import { homedir as homedir3, platform as platform3 } from "node:os";
1648
1769
  var MAX_CONTENT = 48000;
1649
1770
  var MIN_TURN_LEN = 50;
1650
1771
  function resolveProjectName(dirName) {
1651
1772
  const segments = dirName.replace(/^-/, "").split("-");
1652
- const isWindows = platform2() === "win32";
1773
+ const isWindows = platform3() === "win32";
1653
1774
  let resolved;
1654
1775
  let startIdx;
1655
1776
  if (isWindows && segments.length > 0 && /^[A-Za-z]$/.test(segments[0])) {
@@ -1735,6 +1856,22 @@ function parseSession(lines) {
1735
1856
  })).filter((t) => t.assistant.length >= MIN_TURN_LEN);
1736
1857
  return { turns, date };
1737
1858
  }
1859
+ function getParentUuid(lines) {
1860
+ for (const line of lines) {
1861
+ if (!line.trim())
1862
+ continue;
1863
+ let obj;
1864
+ try {
1865
+ obj = JSON.parse(line);
1866
+ } catch {
1867
+ continue;
1868
+ }
1869
+ if (obj && typeof obj === "object" && "parentUuid" in obj) {
1870
+ return obj.parentUuid;
1871
+ }
1872
+ }
1873
+ return;
1874
+ }
1738
1875
  function ingestClaude() {
1739
1876
  const projectsDir = join3(homedir3(), ".claude", "projects");
1740
1877
  const memories = [];
@@ -1759,8 +1896,14 @@ function ingestClaude() {
1759
1896
  for (const file of files) {
1760
1897
  const sessionId = basename(file, ".jsonl");
1761
1898
  const raw = readFileSync3(join3(projPath, file), "utf-8");
1762
- const { turns, date } = parseSession(raw.split(`
1763
- `));
1899
+ const lines = raw.split(`
1900
+ `);
1901
+ const parentUuid = getParentUuid(lines);
1902
+ if (parentUuid != null) {
1903
+ filtered++;
1904
+ continue;
1905
+ }
1906
+ const { turns, date } = parseSession(lines);
1764
1907
  if (turns.length === 0) {
1765
1908
  filtered++;
1766
1909
  continue;
@@ -2139,15 +2282,14 @@ init_api();
2139
2282
  // src/configure.ts
2140
2283
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2, symlinkSync, readlinkSync, rmSync } from "node:fs";
2141
2284
  import { dirname, join as join7 } from "node:path";
2142
- import { homedir as homedir5, platform as platform4 } from "node:os";
2285
+ import { homedir as homedir5, platform as platform5 } from "node:os";
2143
2286
  import { spawnSync } from "node:child_process";
2144
2287
  var CONARE_URL = "https://mcp.conare.ai";
2145
- var SERVER_NAME = "conare-memory";
2288
+ var SERVER_NAME = "conare";
2146
2289
  var MCP_TARGETS = [
2147
2290
  { id: "claude", label: "Claude Code" },
2148
2291
  { id: "cursor", label: "Cursor" },
2149
- { id: "codex", label: "Codex" },
2150
- { id: "openclaw", label: "OpenClaw" }
2292
+ { id: "codex", label: "Codex" }
2151
2293
  ];
2152
2294
  function readJsonFile(path) {
2153
2295
  try {
@@ -2183,7 +2325,7 @@ function configureClaude(apiKey) {
2183
2325
  const claudeMcpPath = join7(homedir5(), ".claude", "mcp.json");
2184
2326
  if (spawnSync("claude", ["mcp", "add-json", SERVER_NAME, "--scope", "user", JSON.stringify(getServerConfig(apiKey))], {
2185
2327
  stdio: "ignore",
2186
- shell: platform4() === "win32"
2328
+ shell: platform5() === "win32"
2187
2329
  }).status === 0) {
2188
2330
  return "Claude Code configured via `claude mcp add-json`";
2189
2331
  }
@@ -2198,41 +2340,166 @@ function configureJsonClient(path, apiKey, label) {
2198
2340
  return `${label} configured at ${path}`;
2199
2341
  }
2200
2342
  var SKILL_MD = `---
2201
- slug: conare-memory
2202
- name: Conare Memory
2203
- description: Long-term memory for AI agents automatically recalls past conversations, saves preferences, and searches across all your coding sessions. Works with Claude Code, Cursor, Codex, and more.
2343
+ name: conare
2344
+ description: Complements the Conare MCP server by teaching the agent when and how to load prior project context, search past sessions, save durable preferences, list stored memories, and forget saved items. Use when the user asks what they worked on before, wants prior context loaded at the start of a task, asks to remember or forget something, or needs past conversations, decisions, or code recalled from memory.
2345
+ compatibility: Requires the Conare MCP server tools (\`recall\`, \`search\`, \`save\`, \`list\`, \`forget\`) to be installed and connected.
2346
+ metadata:
2347
+ author: Conare
2348
+ version: 1.1.0
2349
+ mcp-server: conare
2204
2350
  homepage: https://conare.ai
2205
2351
  ---
2206
2352
 
2207
- # Conare Memory
2353
+ # Conare
2354
+
2355
+ This skill complements the Conare MCP server. The MCP provides memory tools and live access to stored memories; this skill teaches the agent the default workflow, tool-selection rules, and query patterns that make those tools reliable.
2208
2356
 
2209
- Persistent memory across coding sessions. Your conversations, decisions, and preferences are automatically indexed and recalled in future sessions.
2357
+ ## Primary Use Cases
2358
+
2359
+ 1. Start a new coding task with relevant history already loaded through \`recall\`.
2360
+ 2. Answer questions about prior work, decisions, bugs, architecture, or preferences through \`search\`.
2361
+ 3. Persist durable information the user wants carried into future sessions through \`save\`.
2210
2362
 
2211
2363
  ## When To Use Each Tool
2212
2364
 
2213
2365
  | Situation | Tool | Example |
2214
2366
  |-----------|------|---------|
2215
2367
  | Start of conversation | \`recall\` | Always call first with conversation context |
2216
- | User asks about past work | \`memory_search\` | "What did we do last week?" |
2217
- | User says "remember this" | \`memory_save\` | Save preferences, rules, decisions |
2218
- | User says "forget this" | \`memory_forget\` | Remove a specific memory |
2219
- | Browse what's stored | \`memory_list\` | "Show me recent memories" |
2368
+ | User asks about past work | \`search\` | "What did we do last week?" |
2369
+ | User says "remember this" | \`save\` | Save preferences, rules, decisions |
2370
+ | User says "forget this" | \`forget\` | Remove a specific memory |
2371
+ | Browse what's stored | \`list\` | "Show me recent memories" |
2220
2372
 
2221
2373
  ## Critical Rules
2222
2374
 
2223
2375
  1. **Always call \`recall\` at conversation start** — pass a specific description of the conversation topic, not generic text
2224
- 2. **\`memory_search\` is global** — it searches ALL memories across all projects. Use it when the user asks about specific topics or past conversations
2376
+ 2. **\`search\` is global** — it searches ALL memories across all projects. Use it when the user asks about specific topics or past conversations
2225
2377
  3. **\`recall\` is scoped** — it returns project-relevant context + recent sessions + preferences. Use the \`project\` param when available
2226
2378
  4. **Write descriptive queries** — "how does the billing webhook handle refunds" beats "billing"
2227
2379
  5. **Rephrase and retry** — if first search misses, try different phrasing. Semantic search responds to synonyms and related concepts
2228
2380
 
2381
+ ## Workflow
2382
+
2383
+ ### Step 1: Load context at the start
2384
+
2385
+ - Call \`recall\` at conversation start with a specific description of the current task.
2386
+ - Include the \`project\` parameter when the project is known or inferable from the workspace.
2387
+ - Use the returned context to avoid re-asking for things the user already told the agent in earlier sessions.
2388
+
2389
+ Expected outcome: the agent begins with recent sessions, saved preferences, and project-relevant memory already in context.
2390
+
2391
+ ### Step 2: Search when the user asks about prior work
2392
+
2393
+ - Use \`search\` for questions about past conversations, earlier implementations, prior bugs, design decisions, or work done in a time range.
2394
+ - Start with a descriptive natural-language query.
2395
+ - If results are weak, retry with 2-3 rephrasings from different angles.
2396
+
2397
+ Expected outcome: the agent can cite or summarize the most relevant prior work without broad manual browsing.
2398
+
2399
+ ### Step 3: Save durable facts intentionally
2400
+
2401
+ - Use \`save\` proactively for information that should persist across sessions: preferences, standing rules, important decisions, long-lived project facts, and user-specific context that will help in future work.
2402
+ - When the user shares durable context or says to remember something, prefer capturing it with \`save\` so future \`recall\` calls can surface it automatically.
2403
+ - Avoid cluttering memory with purely transient scratch notes unless the user explicitly wants them remembered.
2404
+
2405
+ Expected outcome: future \`recall\` calls surface the information automatically when relevant.
2406
+
2229
2407
  ## Search Tips
2230
2408
 
2231
2409
  - Use \`after\`/\`before\` (Unix ms) for time-scoped searches: "what did we work on this week?"
2232
2410
  - Use \`containerTag\` to filter by source: \`claude-chats\`, \`codex-chats\`, \`cursor-chats\`, \`codebase\`, \`preferences\`
2233
2411
  - Use \`project\` for cross-project filtering (partial names work: "conare" matches "fun/conare-memory-engine")
2234
2412
  - Keep \`limit\` low (3-5) for focused results, higher (10-15) for broad exploration
2235
- - When user asks to "remember" something, save it with \`memory_save\` — it goes to the \`preferences\` container and gets surfaced by \`recall\` in future sessions
2413
+ - When user asks to "remember" something, save it with \`save\` — it goes to the \`preferences\` container and gets surfaced by \`recall\` in future sessions
2414
+
2415
+ ## Examples
2416
+
2417
+ ### Example 1: Start with context
2418
+
2419
+ User says: "Help me continue the OAuth migration in this repo."
2420
+
2421
+ Actions:
2422
+ 1. Call \`recall\` with context like "continue OAuth migration in the current repository".
2423
+ 2. Review returned memories for prior migration decisions, unresolved blockers, and saved preferences.
2424
+ 3. Proceed with implementation using that context.
2425
+
2426
+ Result: the agent starts with the relevant project history instead of asking the user to restate it.
2427
+
2428
+ ### Example 2: Find prior work
2429
+
2430
+ User says: "What did we decide last week about billing webhooks?"
2431
+
2432
+ Actions:
2433
+ 1. Call \`search\` with a descriptive query such as "billing webhook decision refunds retries last week".
2434
+ 2. If results are weak, retry with alternatives like "refund webhook handling" or "billing retry policy".
2435
+ 3. Summarize the decision and note uncertainty if memories conflict.
2436
+
2437
+ Result: the agent retrieves prior decisions from memory rather than guessing.
2438
+
2439
+ ### Example 3: Save a durable preference
2440
+
2441
+ User says: "Remember that I prefer ripgrep over grep."
2442
+
2443
+ Actions:
2444
+ 1. Call \`save\` with the preference in durable wording.
2445
+ 2. Confirm the preference was saved.
2446
+
2447
+ Result: future \`recall\` results can surface that preference automatically.
2448
+
2449
+ ## Troubleshooting
2450
+
2451
+ ### Weak or irrelevant search results
2452
+
2453
+ Cause:
2454
+ - Query is too short or too generic.
2455
+ - Search needs a different phrasing or time scope.
2456
+
2457
+ Response:
2458
+ 1. Rewrite the query with concrete nouns, entities, and actions.
2459
+ 2. Add \`after\`/\`before\` when the user implies a time window.
2460
+ 3. Add \`project\` or \`containerTag\` when the scope is known.
2461
+
2462
+ ### \`recall\` returns little useful context
2463
+
2464
+ Cause:
2465
+ - Conversation context passed to \`recall\` was too vague.
2466
+ - The project parameter was omitted when it should have been included.
2467
+
2468
+ Response:
2469
+ 1. Retry \`recall\` with a more specific task description.
2470
+ 2. Include \`project\` if the workspace or repository is known.
2471
+ 3. Fall back to \`search\` for the exact topic the user cares about.
2472
+
2473
+ ### Unsure whether to save something
2474
+
2475
+ Cause:
2476
+ - The information may be temporary rather than durable.
2477
+
2478
+ Response:
2479
+ 1. Save it when it is likely to help in future sessions, especially if it reflects a preference, rule, decision, or durable project fact.
2480
+ 2. Skip only clearly transient implementation notes unless the user explicitly asks to retain them.
2481
+
2482
+ ## Triggering Tests
2483
+
2484
+ Use these tests to validate whether the skill description is tuned correctly.
2485
+
2486
+ Should trigger:
2487
+ - "What did we work on in this repo last week?"
2488
+ - "Load context for the auth refactor before we continue."
2489
+ - "Remember that I prefer pnpm in JavaScript projects."
2490
+ - "Forget the note about using staging Stripe keys."
2491
+ - "Search my past sessions for the DO migration fix."
2492
+
2493
+ Should not trigger:
2494
+ - "Write a sorting function in TypeScript."
2495
+ - "What is the weather in San Francisco?"
2496
+ - "Explain how PostgreSQL indexes work."
2497
+ - "Create a landing page from this mockup."
2498
+
2499
+ Success criteria:
2500
+ - Triggers on memory-oriented requests and start-of-task context loading.
2501
+ - Does not trigger on generic coding or research tasks with no memory component.
2502
+ - Uses \`recall\` first for new task continuation, and uses \`search\` only when the user asks about specific past work.
2236
2503
 
2237
2504
  ## Setup
2238
2505
 
@@ -2247,11 +2514,11 @@ The wizard handles everything: account creation, API key, MCP configuration, bac
2247
2514
  For manual setup, visit [conare.ai](https://conare.ai).
2248
2515
  `;
2249
2516
  function installSkill() {
2250
- const skillDir = join7(homedir5(), ".agents", "skills", "conare-memory");
2517
+ const skillDir = join7(homedir5(), ".agents", "skills", "conare");
2251
2518
  mkdirSync2(skillDir, { recursive: true });
2252
2519
  writeFileSync2(join7(skillDir, "SKILL.md"), SKILL_MD);
2253
2520
  const claudeSkillsDir = join7(homedir5(), ".claude", "skills");
2254
- const claudeSkillDir = join7(claudeSkillsDir, "conare-memory");
2521
+ const claudeSkillDir = join7(claudeSkillsDir, "conare");
2255
2522
  try {
2256
2523
  if (existsSync6(claudeSkillsDir)) {
2257
2524
  if (existsSync6(claudeSkillDir)) {
@@ -2277,9 +2544,6 @@ function configureMcp(apiKey, targets = ["claude", "cursor", "codex"]) {
2277
2544
  if (targets.includes("codex")) {
2278
2545
  results.push(configureJsonClient(join7(homedir5(), ".codex", "mcp.json"), apiKey, "Codex"));
2279
2546
  }
2280
- if (targets.includes("openclaw")) {
2281
- results.push(configureJsonClient(join7(homedir5(), ".openclaw", "mcp.json"), apiKey, "OpenClaw"));
2282
- }
2283
2547
  try {
2284
2548
  results.push(installSkill());
2285
2549
  } catch {}
@@ -2301,20 +2565,35 @@ function readConfig() {
2301
2565
  return {};
2302
2566
  }
2303
2567
  }
2568
+ function writeConfig(config) {
2569
+ mkdirSync3(CONFIG_DIR, { recursive: true, mode: 448 });
2570
+ writeFileSync3(CONFIG_PATH, JSON.stringify(config, null, 2) + `
2571
+ `, { mode: 384 });
2572
+ }
2304
2573
  function saveApiKey(apiKey) {
2305
- mkdirSync3(CONFIG_DIR, { recursive: true });
2306
- writeFileSync3(CONFIG_PATH, JSON.stringify({ apiKey }, null, 2) + `
2307
- `);
2574
+ const config = readConfig();
2575
+ config.apiKey = apiKey;
2576
+ writeConfig(config);
2308
2577
  }
2309
2578
  function getSavedApiKey() {
2310
2579
  return readConfig().apiKey;
2311
2580
  }
2581
+ function addIndexedPath(absPath) {
2582
+ const config = readConfig();
2583
+ const paths = new Set(config.indexedPaths || []);
2584
+ paths.add(absPath);
2585
+ config.indexedPaths = [...paths];
2586
+ writeConfig(config);
2587
+ }
2588
+ function getIndexedPaths() {
2589
+ return readConfig().indexedPaths || [];
2590
+ }
2312
2591
 
2313
2592
  // src/sync.ts
2314
2593
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync9, chmodSync, cpSync, rmSync as rmSync2, symlinkSync as symlinkSync2, readlinkSync as readlinkSync2, appendFileSync } from "node:fs";
2315
2594
  import { join as join9, dirname as dirname2 } from "node:path";
2316
- import { homedir as homedir7, platform as platform5 } from "node:os";
2317
- import { execSync as execSync2 } from "node:child_process";
2595
+ import { homedir as homedir7, platform as platform6 } from "node:os";
2596
+ import { execSync as execSync3 } from "node:child_process";
2318
2597
  var CONARE_DIR = join9(homedir7(), ".conare");
2319
2598
  var BIN_DIR = join9(CONARE_DIR, "bin");
2320
2599
  var CONFIG_PATH2 = join9(CONARE_DIR, "config.json");
@@ -2470,7 +2749,7 @@ WantedBy=timers.target
2470
2749
  }
2471
2750
  function hasSystemd() {
2472
2751
  try {
2473
- execSync2("systemctl --user status 2>/dev/null", { stdio: "ignore" });
2752
+ execSync3("systemctl --user status 2>/dev/null", { stdio: "ignore" });
2474
2753
  return true;
2475
2754
  } catch {
2476
2755
  return false;
@@ -2478,7 +2757,7 @@ function hasSystemd() {
2478
2757
  }
2479
2758
  function uid() {
2480
2759
  try {
2481
- return execSync2("id -u", { encoding: "utf-8" }).trim();
2760
+ return execSync3("id -u", { encoding: "utf-8" }).trim();
2482
2761
  } catch {
2483
2762
  return "501";
2484
2763
  }
@@ -2510,7 +2789,7 @@ function persistBinary(apiKey) {
2510
2789
  const runVbsPath = join9(BIN_DIR, "run.vbs");
2511
2790
  writeFileSync4(runVbsPath, RUN_VBS);
2512
2791
  writeFileSync4(CONFIG_PATH2, JSON.stringify({ apiKey }, null, 2) + `
2513
- `);
2792
+ `, { mode: 384 });
2514
2793
  }
2515
2794
  function isValidJsBundle(path) {
2516
2795
  if (!existsSync8(path))
@@ -2555,13 +2834,13 @@ function setupMacOS(intervalMinutes) {
2555
2834
  writeFileSync4(PLIST_PATH, makePlist(intervalMinutes));
2556
2835
  const id = uid();
2557
2836
  try {
2558
- execSync2(`launchctl bootout gui/${id} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
2837
+ execSync3(`launchctl bootout gui/${id} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
2559
2838
  } catch {}
2560
2839
  try {
2561
- execSync2(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
2840
+ execSync3(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
2562
2841
  } catch {
2563
2842
  try {
2564
- execSync2(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
2843
+ execSync3(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
2565
2844
  } catch {
2566
2845
  throw new Error("Failed to load launchd agent. Try manually: launchctl load " + PLIST_PATH);
2567
2846
  }
@@ -2571,28 +2850,28 @@ function setupLinuxSystemd(intervalMinutes) {
2571
2850
  mkdirSync4(SYSTEMD_DIR, { recursive: true });
2572
2851
  writeFileSync4(SYSTEMD_SERVICE, SYSTEMD_SERVICE_CONTENT);
2573
2852
  writeFileSync4(SYSTEMD_TIMER, makeSystemdTimer(intervalMinutes));
2574
- execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
2575
- execSync2("systemctl --user enable --now conare-sync.timer", { stdio: "ignore" });
2853
+ execSync3("systemctl --user daemon-reload", { stdio: "ignore" });
2854
+ execSync3("systemctl --user enable --now conare-sync.timer", { stdio: "ignore" });
2576
2855
  }
2577
2856
  function setupLinuxCron(intervalMinutes) {
2578
2857
  const cronCmd = `${homedir7()}/.conare/bin/run.sh`;
2579
2858
  const cronLine = `*/${intervalMinutes} * * * * ${cronCmd}`;
2580
2859
  try {
2581
- const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
2860
+ const existing = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
2582
2861
  const filtered = existing.split(`
2583
2862
  `).filter((l) => !l.includes("conare")).join(`
2584
2863
  `);
2585
2864
  const newCrontab = (filtered.trim() ? filtered.trim() + `
2586
2865
  ` : "") + cronLine + `
2587
2866
  `;
2588
- execSync2("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
2867
+ execSync3("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
2589
2868
  } catch {
2590
- execSync2("crontab -", { input: cronLine + `
2869
+ execSync3("crontab -", { input: cronLine + `
2591
2870
  `, stdio: ["pipe", "ignore", "ignore"] });
2592
2871
  }
2593
2872
  }
2594
2873
  function installGlobalCommand() {
2595
- const isWindows = platform5() === "win32";
2874
+ const isWindows = platform6() === "win32";
2596
2875
  if (isWindows) {
2597
2876
  const wrapper2 = join9(BIN_DIR, "conare.cmd");
2598
2877
  const content2 = `@echo off\r
@@ -2610,7 +2889,7 @@ node "%USERPROFILE%\\.conare\\bin\\conare-ingest.mjs" %*\r
2610
2889
  }
2611
2890
  const binDirWin = BIN_DIR.replace(/\//g, "\\");
2612
2891
  try {
2613
- execSync2(`powershell -NoProfile -Command "$p = [Environment]::GetEnvironmentVariable('PATH','User'); if ($p -and $p.ToLower().Contains('${binDirWin.toLowerCase().replace(/\\/g, "\\\\")}')) { exit 0 }; [Environment]::SetEnvironmentVariable('PATH', $(if($p){$p + ';'} else {''}) + '${binDirWin.replace(/\\/g, "\\\\")}', 'User')"`, { stdio: "ignore" });
2892
+ execSync3(`powershell -NoProfile -Command "$p = [Environment]::GetEnvironmentVariable('PATH','User'); if ($p -and $p.ToLower().Contains('${binDirWin.toLowerCase().replace(/\\/g, "\\\\")}')) { exit 0 }; [Environment]::SetEnvironmentVariable('PATH', $(if($p){$p + ';'} else {''}) + '${binDirWin.replace(/\\/g, "\\\\")}', 'User')"`, { stdio: "ignore" });
2614
2893
  return `Global command: conare (added .conare\\bin to user PATH — restart terminal)`;
2615
2894
  } catch {
2616
2895
  return `Global command: add ${binDirWin} to your PATH manually`;
@@ -2679,7 +2958,7 @@ function getShellProfile() {
2679
2958
  return join9(home, ".zshrc");
2680
2959
  if (shell.includes("bash")) {
2681
2960
  const profile = join9(home, ".bash_profile");
2682
- if (platform5() === "darwin" && existsSync8(profile))
2961
+ if (platform6() === "darwin" && existsSync8(profile))
2683
2962
  return profile;
2684
2963
  return join9(home, ".bashrc");
2685
2964
  }
@@ -2692,9 +2971,9 @@ function getShellProfile() {
2692
2971
  function setupWindows(intervalMinutes) {
2693
2972
  const runVbs = join9(BIN_DIR, "run.vbs").replace(/\//g, "\\");
2694
2973
  try {
2695
- execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2974
+ execSync3(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2696
2975
  } catch {}
2697
- execSync2(`schtasks /Create /TN "${TASK_NAME}" /TR "wscript.exe \\"${runVbs}\\"" /SC MINUTE /MO ${intervalMinutes} /F`, { stdio: "ignore" });
2976
+ execSync3(`schtasks /Create /TN "${TASK_NAME}" /TR "wscript.exe \\"${runVbs}\\"" /SC MINUTE /MO ${intervalMinutes} /F`, { stdio: "ignore" });
2698
2977
  }
2699
2978
  function persistAndInstallGlobal(apiKey) {
2700
2979
  const messages = [];
@@ -2708,7 +2987,7 @@ function persistAndInstallGlobal(apiKey) {
2708
2987
  }
2709
2988
  function installSync(apiKey, intervalMinutes = 10) {
2710
2989
  const messages = persistAndInstallGlobal(apiKey);
2711
- const os = platform5();
2990
+ const os = platform6();
2712
2991
  if (os === "darwin") {
2713
2992
  setupMacOS(intervalMinutes);
2714
2993
  messages.push(`Installed launchd agent (every ${intervalMinutes} min)`);
@@ -2731,10 +3010,10 @@ function installSync(apiKey, intervalMinutes = 10) {
2731
3010
  }
2732
3011
  function uninstallSync() {
2733
3012
  const messages = [];
2734
- const os = platform5();
3013
+ const os = platform6();
2735
3014
  if (os === "darwin") {
2736
3015
  try {
2737
- execSync2(`launchctl bootout gui/${uid()} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
3016
+ execSync3(`launchctl bootout gui/${uid()} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
2738
3017
  } catch {}
2739
3018
  if (existsSync8(PLIST_PATH)) {
2740
3019
  unlinkSync(PLIST_PATH);
@@ -2742,29 +3021,29 @@ function uninstallSync() {
2742
3021
  }
2743
3022
  } else if (os === "win32") {
2744
3023
  try {
2745
- execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
3024
+ execSync3(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2746
3025
  messages.push("Removed Windows scheduled task");
2747
3026
  } catch {}
2748
3027
  } else if (os === "linux") {
2749
3028
  if (hasSystemd()) {
2750
3029
  try {
2751
- execSync2("systemctl --user disable --now conare-sync.timer 2>/dev/null", { stdio: "ignore" });
3030
+ execSync3("systemctl --user disable --now conare-sync.timer 2>/dev/null", { stdio: "ignore" });
2752
3031
  } catch {}
2753
3032
  if (existsSync8(SYSTEMD_SERVICE))
2754
3033
  unlinkSync(SYSTEMD_SERVICE);
2755
3034
  if (existsSync8(SYSTEMD_TIMER))
2756
3035
  unlinkSync(SYSTEMD_TIMER);
2757
3036
  try {
2758
- execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
3037
+ execSync3("systemctl --user daemon-reload", { stdio: "ignore" });
2759
3038
  } catch {}
2760
3039
  messages.push("Removed systemd timer");
2761
3040
  } else {
2762
3041
  try {
2763
- const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
3042
+ const existing = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
2764
3043
  const filtered = existing.split(`
2765
3044
  `).filter((l) => !l.includes("conare")).join(`
2766
3045
  `);
2767
- execSync2("crontab -", { input: filtered.trim() + `
3046
+ execSync3("crontab -", { input: filtered.trim() + `
2768
3047
  `, stdio: ["pipe", "ignore", "ignore"] });
2769
3048
  messages.push("Removed cron job");
2770
3049
  } catch {}
@@ -2899,13 +3178,13 @@ function parseArgs() {
2899
3178
  conare — AI memory for your coding tools
2900
3179
 
2901
3180
  Usage:
2902
- conare install Just install the MCP (simplest)
2903
- conare install --key <api_key> Install MCP with key
2904
- conare --key <api_key> Ingest chat history
3181
+ conare Interactive setup with browser auth
3182
+ conare install Just install the MCP
3183
+ conare --key <api_key> Ingest chat history (key optional with browser auth)
2905
3184
  conare --key <api_key> --index [path] Index codebase
2906
3185
 
2907
3186
  Options:
2908
- --key <key> Your Conare API key (required, starts with cmem_)
3187
+ --key <key> Your Conare API key (optional if using browser auth, starts with cmem_)
2909
3188
  --config-file <path> Read API key from JSON config file (e.g. ~/.conare/config.json)
2910
3189
  --index [path] Index codebase at path (default: current directory)
2911
3190
  --project <name> Project name for codebase indexing (auto-detected if omitted)
@@ -2949,8 +3228,22 @@ async function runInstall() {
2949
3228
  let apiKey = key || process.env.CONARE_API_KEY || savedApiKey;
2950
3229
  const hasTty = !!process.stdin.isTTY && !!process.stdout.isTTY;
2951
3230
  if (!apiKey && hasTty) {
2952
- const { promptApiKey: promptKey } = await Promise.resolve().then(() => (init_interactive(), exports_interactive));
2953
- apiKey = await promptKey({ savedApiKey }) || "";
3231
+ const { promptAuth: promptAuthFn } = await Promise.resolve().then(() => (init_interactive(), exports_interactive));
3232
+ const { spinner: clackSpinner } = await Promise.resolve().then(() => (init_interactive(), exports_interactive));
3233
+ const authResult = await promptAuthFn({ savedApiKey });
3234
+ if (authResult === "__BROWSER_AUTH__") {
3235
+ const authSpinner = clackSpinner();
3236
+ authSpinner.start("Waiting for browser sign-in...");
3237
+ try {
3238
+ apiKey = await browserAuth();
3239
+ authSpinner.stop("Signed in successfully.");
3240
+ } catch (e2) {
3241
+ authSpinner.stop(`Authentication failed: ${e2.message}`);
3242
+ process.exit(1);
3243
+ }
3244
+ } else {
3245
+ apiKey = authResult || "";
3246
+ }
2954
3247
  }
2955
3248
  if (!apiKey) {
2956
3249
  printMissingKeyError();
@@ -3013,7 +3306,7 @@ async function main() {
3013
3306
  if (shouldRunInteractive) {
3014
3307
  const detectedTools = await detect();
3015
3308
  interactiveTargets = MCP_TARGETS.map((target) => {
3016
- const detected = detectedTools.find((tool) => target.id === "claude" && tool.name === "Claude Code" || target.id === "cursor" && tool.name === "Cursor" || target.id === "codex" && tool.name === "Codex" || target.id === "openclaw" && tool.name === "OpenClaw");
3309
+ const detected = detectedTools.find((tool) => target.id === "claude" && tool.name === "Claude Code" || target.id === "cursor" && tool.name === "Cursor" || target.id === "codex" && tool.name === "Codex");
3017
3310
  return {
3018
3311
  id: target.id,
3019
3312
  label: target.label,
@@ -3023,10 +3316,23 @@ async function main() {
3023
3316
  };
3024
3317
  });
3025
3318
  startSetup();
3026
- apiKey = await promptApiKey({
3319
+ const authResult = await promptAuth({
3027
3320
  savedApiKey,
3028
3321
  providedApiKey: opts.key
3029
- }) || apiKey;
3322
+ });
3323
+ if (authResult === "__BROWSER_AUTH__") {
3324
+ const authSpinner = Y2();
3325
+ authSpinner.start("Waiting for browser sign-in...");
3326
+ try {
3327
+ apiKey = await browserAuth();
3328
+ authSpinner.stop("Signed in successfully.");
3329
+ } catch (e2) {
3330
+ authSpinner.stop(`Authentication failed: ${e2.message}`);
3331
+ process.exit(1);
3332
+ }
3333
+ } else {
3334
+ apiKey = authResult || apiKey;
3335
+ }
3030
3336
  showDetectedApps(interactiveTargets);
3031
3337
  selectedSources = await selectChatSources(interactiveTargets);
3032
3338
  interactiveMode = true;
@@ -3038,8 +3344,20 @@ async function main() {
3038
3344
  return;
3039
3345
  }
3040
3346
  if (!apiKey) {
3041
- printMissingKeyError();
3042
- process.exit(1);
3347
+ if (hasTty) {
3348
+ const authSpinner = Y2();
3349
+ authSpinner.start("Waiting for browser sign-in...");
3350
+ try {
3351
+ apiKey = await browserAuth();
3352
+ authSpinner.stop("Signed in successfully.");
3353
+ } catch (e2) {
3354
+ authSpinner.stop(`Authentication failed: ${e2.message}`);
3355
+ process.exit(1);
3356
+ }
3357
+ } else {
3358
+ printMissingKeyError();
3359
+ process.exit(1);
3360
+ }
3043
3361
  }
3044
3362
  if (opts.installSync) {
3045
3363
  const auth2 = await validateKey(apiKey);
@@ -3162,6 +3480,7 @@ Nothing new to index.`);
3162
3480
  write(renderProgressSummary(success, failed, "indexed"));
3163
3481
  const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
3164
3482
  markIngested("codebase", fileHashes);
3483
+ addIndexedPath(absPath);
3165
3484
  if (!opts.quiet)
3166
3485
  printFailureSummary(results, memories);
3167
3486
  }
@@ -3293,6 +3612,24 @@ Nothing new to index.`);
3293
3612
  }
3294
3613
  }
3295
3614
  log();
3615
+ if (effectiveIngestOnly && !opts.dryRun) {
3616
+ const savedPaths = getIndexedPaths();
3617
+ for (const savedPath of savedPaths) {
3618
+ if (!existsSync9(savedPath))
3619
+ continue;
3620
+ try {
3621
+ const { memories: codeMemories, project } = indexCodebase(savedPath, { changedOnly: true });
3622
+ if (codeMemories.length === 0)
3623
+ continue;
3624
+ log(` Re-indexing ${project}: ${codeMemories.length} changed files`);
3625
+ const { success, failed, results } = await uploadBulk(apiKey, codeMemories, () => {});
3626
+ if (!opts.quiet)
3627
+ log(` ${project}: ${success} indexed, ${failed} failed`);
3628
+ const fileHashes = results.filter((r2) => r2.success).map((r2) => getManifestFingerprint(codeMemories[r2.index])).filter((k3) => !!k3);
3629
+ markIngested("codebase", fileHashes);
3630
+ } catch {}
3631
+ }
3632
+ }
3296
3633
  if (interactiveMode) {
3297
3634
  selectedTargets = await selectMcpTargets(interactiveTargets);
3298
3635
  const shouldIndexCurrentCodebase = await confirmIndexCodebase();
@@ -3336,6 +3673,7 @@ Nothing new to index.`);
3336
3673
  write(renderProgressSummary(success, failed, "indexed"));
3337
3674
  const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
3338
3675
  markIngested("codebase", fileHashes);
3676
+ addIndexedPath(absPath);
3339
3677
  if (!opts.quiet)
3340
3678
  printFailureSummary(results, memories);
3341
3679
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conare",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Conare CLI for ingesting AI chat history and configuring memory at conare.ai",
5
5
  "type": "module",
6
6
  "bin": {