conare 0.3.4 → 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 +479 -69
  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;
@@ -2137,17 +2280,16 @@ init_shared();
2137
2280
  init_api();
2138
2281
 
2139
2282
  // src/configure.ts
2140
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
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
  }
@@ -2197,6 +2339,200 @@ function configureJsonClient(path, apiKey, label) {
2197
2339
  upsertMcpServer(path, apiKey);
2198
2340
  return `${label} configured at ${path}`;
2199
2341
  }
2342
+ var SKILL_MD = `---
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
2350
+ homepage: https://conare.ai
2351
+ ---
2352
+
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.
2356
+
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\`.
2362
+
2363
+ ## When To Use Each Tool
2364
+
2365
+ | Situation | Tool | Example |
2366
+ |-----------|------|---------|
2367
+ | Start of conversation | \`recall\` | Always call first with conversation context |
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" |
2372
+
2373
+ ## Critical Rules
2374
+
2375
+ 1. **Always call \`recall\` at conversation start** — pass a specific description of the conversation topic, not generic text
2376
+ 2. **\`search\` is global** — it searches ALL memories across all projects. Use it when the user asks about specific topics or past conversations
2377
+ 3. **\`recall\` is scoped** — it returns project-relevant context + recent sessions + preferences. Use the \`project\` param when available
2378
+ 4. **Write descriptive queries** — "how does the billing webhook handle refunds" beats "billing"
2379
+ 5. **Rephrase and retry** — if first search misses, try different phrasing. Semantic search responds to synonyms and related concepts
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
+
2407
+ ## Search Tips
2408
+
2409
+ - Use \`after\`/\`before\` (Unix ms) for time-scoped searches: "what did we work on this week?"
2410
+ - Use \`containerTag\` to filter by source: \`claude-chats\`, \`codex-chats\`, \`cursor-chats\`, \`codebase\`, \`preferences\`
2411
+ - Use \`project\` for cross-project filtering (partial names work: "conare" matches "fun/conare-memory-engine")
2412
+ - Keep \`limit\` low (3-5) for focused results, higher (10-15) for broad exploration
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.
2503
+
2504
+ ## Setup
2505
+
2506
+ Install with a single command:
2507
+
2508
+ \`\`\`bash
2509
+ bunx conare@latest
2510
+ \`\`\`
2511
+
2512
+ The wizard handles everything: account creation, API key, MCP configuration, background sync setup.
2513
+
2514
+ For manual setup, visit [conare.ai](https://conare.ai).
2515
+ `;
2516
+ function installSkill() {
2517
+ const skillDir = join7(homedir5(), ".agents", "skills", "conare");
2518
+ mkdirSync2(skillDir, { recursive: true });
2519
+ writeFileSync2(join7(skillDir, "SKILL.md"), SKILL_MD);
2520
+ const claudeSkillsDir = join7(homedir5(), ".claude", "skills");
2521
+ const claudeSkillDir = join7(claudeSkillsDir, "conare");
2522
+ try {
2523
+ if (existsSync6(claudeSkillsDir)) {
2524
+ if (existsSync6(claudeSkillDir)) {
2525
+ try {
2526
+ if (readlinkSync(claudeSkillDir) === skillDir)
2527
+ return "Agent Skill installed";
2528
+ } catch {}
2529
+ rmSync(claudeSkillDir, { recursive: true, force: true });
2530
+ }
2531
+ symlinkSync(skillDir, claudeSkillDir);
2532
+ }
2533
+ } catch {}
2534
+ return "Agent Skill installed";
2535
+ }
2200
2536
  function configureMcp(apiKey, targets = ["claude", "cursor", "codex"]) {
2201
2537
  const results = [];
2202
2538
  if (targets.includes("claude")) {
@@ -2208,9 +2544,9 @@ function configureMcp(apiKey, targets = ["claude", "cursor", "codex"]) {
2208
2544
  if (targets.includes("codex")) {
2209
2545
  results.push(configureJsonClient(join7(homedir5(), ".codex", "mcp.json"), apiKey, "Codex"));
2210
2546
  }
2211
- if (targets.includes("openclaw")) {
2212
- results.push(configureJsonClient(join7(homedir5(), ".openclaw", "mcp.json"), apiKey, "OpenClaw"));
2213
- }
2547
+ try {
2548
+ results.push(installSkill());
2549
+ } catch {}
2214
2550
  return results;
2215
2551
  }
2216
2552
 
@@ -2229,20 +2565,35 @@ function readConfig() {
2229
2565
  return {};
2230
2566
  }
2231
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
+ }
2232
2573
  function saveApiKey(apiKey) {
2233
- mkdirSync3(CONFIG_DIR, { recursive: true });
2234
- writeFileSync3(CONFIG_PATH, JSON.stringify({ apiKey }, null, 2) + `
2235
- `);
2574
+ const config = readConfig();
2575
+ config.apiKey = apiKey;
2576
+ writeConfig(config);
2236
2577
  }
2237
2578
  function getSavedApiKey() {
2238
2579
  return readConfig().apiKey;
2239
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
+ }
2240
2591
 
2241
2592
  // src/sync.ts
2242
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync9, chmodSync, cpSync, rmSync, symlinkSync, readlinkSync, appendFileSync } from "node:fs";
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";
2243
2594
  import { join as join9, dirname as dirname2 } from "node:path";
2244
- import { homedir as homedir7, platform as platform5 } from "node:os";
2245
- 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";
2246
2597
  var CONARE_DIR = join9(homedir7(), ".conare");
2247
2598
  var BIN_DIR = join9(CONARE_DIR, "bin");
2248
2599
  var CONFIG_PATH2 = join9(CONARE_DIR, "config.json");
@@ -2398,7 +2749,7 @@ WantedBy=timers.target
2398
2749
  }
2399
2750
  function hasSystemd() {
2400
2751
  try {
2401
- execSync2("systemctl --user status 2>/dev/null", { stdio: "ignore" });
2752
+ execSync3("systemctl --user status 2>/dev/null", { stdio: "ignore" });
2402
2753
  return true;
2403
2754
  } catch {
2404
2755
  return false;
@@ -2406,7 +2757,7 @@ function hasSystemd() {
2406
2757
  }
2407
2758
  function uid() {
2408
2759
  try {
2409
- return execSync2("id -u", { encoding: "utf-8" }).trim();
2760
+ return execSync3("id -u", { encoding: "utf-8" }).trim();
2410
2761
  } catch {
2411
2762
  return "501";
2412
2763
  }
@@ -2438,7 +2789,7 @@ function persistBinary(apiKey) {
2438
2789
  const runVbsPath = join9(BIN_DIR, "run.vbs");
2439
2790
  writeFileSync4(runVbsPath, RUN_VBS);
2440
2791
  writeFileSync4(CONFIG_PATH2, JSON.stringify({ apiKey }, null, 2) + `
2441
- `);
2792
+ `, { mode: 384 });
2442
2793
  }
2443
2794
  function isValidJsBundle(path) {
2444
2795
  if (!existsSync8(path))
@@ -2483,13 +2834,13 @@ function setupMacOS(intervalMinutes) {
2483
2834
  writeFileSync4(PLIST_PATH, makePlist(intervalMinutes));
2484
2835
  const id = uid();
2485
2836
  try {
2486
- 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" });
2487
2838
  } catch {}
2488
2839
  try {
2489
- execSync2(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
2840
+ execSync3(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
2490
2841
  } catch {
2491
2842
  try {
2492
- execSync2(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
2843
+ execSync3(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
2493
2844
  } catch {
2494
2845
  throw new Error("Failed to load launchd agent. Try manually: launchctl load " + PLIST_PATH);
2495
2846
  }
@@ -2499,28 +2850,28 @@ function setupLinuxSystemd(intervalMinutes) {
2499
2850
  mkdirSync4(SYSTEMD_DIR, { recursive: true });
2500
2851
  writeFileSync4(SYSTEMD_SERVICE, SYSTEMD_SERVICE_CONTENT);
2501
2852
  writeFileSync4(SYSTEMD_TIMER, makeSystemdTimer(intervalMinutes));
2502
- execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
2503
- 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" });
2504
2855
  }
2505
2856
  function setupLinuxCron(intervalMinutes) {
2506
2857
  const cronCmd = `${homedir7()}/.conare/bin/run.sh`;
2507
2858
  const cronLine = `*/${intervalMinutes} * * * * ${cronCmd}`;
2508
2859
  try {
2509
- const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
2860
+ const existing = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
2510
2861
  const filtered = existing.split(`
2511
2862
  `).filter((l) => !l.includes("conare")).join(`
2512
2863
  `);
2513
2864
  const newCrontab = (filtered.trim() ? filtered.trim() + `
2514
2865
  ` : "") + cronLine + `
2515
2866
  `;
2516
- execSync2("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
2867
+ execSync3("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
2517
2868
  } catch {
2518
- execSync2("crontab -", { input: cronLine + `
2869
+ execSync3("crontab -", { input: cronLine + `
2519
2870
  `, stdio: ["pipe", "ignore", "ignore"] });
2520
2871
  }
2521
2872
  }
2522
2873
  function installGlobalCommand() {
2523
- const isWindows = platform5() === "win32";
2874
+ const isWindows = platform6() === "win32";
2524
2875
  if (isWindows) {
2525
2876
  const wrapper2 = join9(BIN_DIR, "conare.cmd");
2526
2877
  const content2 = `@echo off\r
@@ -2538,7 +2889,7 @@ node "%USERPROFILE%\\.conare\\bin\\conare-ingest.mjs" %*\r
2538
2889
  }
2539
2890
  const binDirWin = BIN_DIR.replace(/\//g, "\\");
2540
2891
  try {
2541
- 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" });
2542
2893
  return `Global command: conare (added .conare\\bin to user PATH — restart terminal)`;
2543
2894
  } catch {
2544
2895
  return `Global command: add ${binDirWin} to your PATH manually`;
@@ -2567,13 +2918,13 @@ exec "$NODE" "$CONARE_DIR/bin/conare-ingest.mjs" "$@"
2567
2918
  try {
2568
2919
  if (existsSync8(symlinkTarget)) {
2569
2920
  try {
2570
- const existing = readlinkSync(symlinkTarget);
2921
+ const existing = readlinkSync2(symlinkTarget);
2571
2922
  if (existing === wrapper)
2572
2923
  return "Global command: conare (already linked)";
2573
2924
  } catch {}
2574
2925
  unlinkSync(symlinkTarget);
2575
2926
  }
2576
- symlinkSync(wrapper, symlinkTarget);
2927
+ symlinkSync2(wrapper, symlinkTarget);
2577
2928
  return "Global command: conare (linked to /usr/local/bin)";
2578
2929
  } catch {
2579
2930
  const pathDirs = (process.env.PATH || "").split(":");
@@ -2607,7 +2958,7 @@ function getShellProfile() {
2607
2958
  return join9(home, ".zshrc");
2608
2959
  if (shell.includes("bash")) {
2609
2960
  const profile = join9(home, ".bash_profile");
2610
- if (platform5() === "darwin" && existsSync8(profile))
2961
+ if (platform6() === "darwin" && existsSync8(profile))
2611
2962
  return profile;
2612
2963
  return join9(home, ".bashrc");
2613
2964
  }
@@ -2620,9 +2971,9 @@ function getShellProfile() {
2620
2971
  function setupWindows(intervalMinutes) {
2621
2972
  const runVbs = join9(BIN_DIR, "run.vbs").replace(/\//g, "\\");
2622
2973
  try {
2623
- execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2974
+ execSync3(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2624
2975
  } catch {}
2625
- 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" });
2626
2977
  }
2627
2978
  function persistAndInstallGlobal(apiKey) {
2628
2979
  const messages = [];
@@ -2636,7 +2987,7 @@ function persistAndInstallGlobal(apiKey) {
2636
2987
  }
2637
2988
  function installSync(apiKey, intervalMinutes = 10) {
2638
2989
  const messages = persistAndInstallGlobal(apiKey);
2639
- const os = platform5();
2990
+ const os = platform6();
2640
2991
  if (os === "darwin") {
2641
2992
  setupMacOS(intervalMinutes);
2642
2993
  messages.push(`Installed launchd agent (every ${intervalMinutes} min)`);
@@ -2659,10 +3010,10 @@ function installSync(apiKey, intervalMinutes = 10) {
2659
3010
  }
2660
3011
  function uninstallSync() {
2661
3012
  const messages = [];
2662
- const os = platform5();
3013
+ const os = platform6();
2663
3014
  if (os === "darwin") {
2664
3015
  try {
2665
- 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" });
2666
3017
  } catch {}
2667
3018
  if (existsSync8(PLIST_PATH)) {
2668
3019
  unlinkSync(PLIST_PATH);
@@ -2670,29 +3021,29 @@ function uninstallSync() {
2670
3021
  }
2671
3022
  } else if (os === "win32") {
2672
3023
  try {
2673
- execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
3024
+ execSync3(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2674
3025
  messages.push("Removed Windows scheduled task");
2675
3026
  } catch {}
2676
3027
  } else if (os === "linux") {
2677
3028
  if (hasSystemd()) {
2678
3029
  try {
2679
- 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" });
2680
3031
  } catch {}
2681
3032
  if (existsSync8(SYSTEMD_SERVICE))
2682
3033
  unlinkSync(SYSTEMD_SERVICE);
2683
3034
  if (existsSync8(SYSTEMD_TIMER))
2684
3035
  unlinkSync(SYSTEMD_TIMER);
2685
3036
  try {
2686
- execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
3037
+ execSync3("systemctl --user daemon-reload", { stdio: "ignore" });
2687
3038
  } catch {}
2688
3039
  messages.push("Removed systemd timer");
2689
3040
  } else {
2690
3041
  try {
2691
- const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
3042
+ const existing = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
2692
3043
  const filtered = existing.split(`
2693
3044
  `).filter((l) => !l.includes("conare")).join(`
2694
3045
  `);
2695
- execSync2("crontab -", { input: filtered.trim() + `
3046
+ execSync3("crontab -", { input: filtered.trim() + `
2696
3047
  `, stdio: ["pipe", "ignore", "ignore"] });
2697
3048
  messages.push("Removed cron job");
2698
3049
  } catch {}
@@ -2707,7 +3058,7 @@ function uninstallSync() {
2707
3058
  unlinkSync(f);
2708
3059
  }
2709
3060
  if (existsSync8(BIN_DIR)) {
2710
- rmSync(BIN_DIR, { recursive: true, force: true });
3061
+ rmSync2(BIN_DIR, { recursive: true, force: true });
2711
3062
  messages.push("Removed ~/.conare/bin/");
2712
3063
  }
2713
3064
  if (messages.length === 0) {
@@ -2827,13 +3178,13 @@ function parseArgs() {
2827
3178
  conare — AI memory for your coding tools
2828
3179
 
2829
3180
  Usage:
2830
- conare install Just install the MCP (simplest)
2831
- conare install --key <api_key> Install MCP with key
2832
- 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)
2833
3184
  conare --key <api_key> --index [path] Index codebase
2834
3185
 
2835
3186
  Options:
2836
- --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_)
2837
3188
  --config-file <path> Read API key from JSON config file (e.g. ~/.conare/config.json)
2838
3189
  --index [path] Index codebase at path (default: current directory)
2839
3190
  --project <name> Project name for codebase indexing (auto-detected if omitted)
@@ -2877,8 +3228,22 @@ async function runInstall() {
2877
3228
  let apiKey = key || process.env.CONARE_API_KEY || savedApiKey;
2878
3229
  const hasTty = !!process.stdin.isTTY && !!process.stdout.isTTY;
2879
3230
  if (!apiKey && hasTty) {
2880
- const { promptApiKey: promptKey } = await Promise.resolve().then(() => (init_interactive(), exports_interactive));
2881
- 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
+ }
2882
3247
  }
2883
3248
  if (!apiKey) {
2884
3249
  printMissingKeyError();
@@ -2941,7 +3306,7 @@ async function main() {
2941
3306
  if (shouldRunInteractive) {
2942
3307
  const detectedTools = await detect();
2943
3308
  interactiveTargets = MCP_TARGETS.map((target) => {
2944
- 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");
2945
3310
  return {
2946
3311
  id: target.id,
2947
3312
  label: target.label,
@@ -2951,10 +3316,23 @@ async function main() {
2951
3316
  };
2952
3317
  });
2953
3318
  startSetup();
2954
- apiKey = await promptApiKey({
3319
+ const authResult = await promptAuth({
2955
3320
  savedApiKey,
2956
3321
  providedApiKey: opts.key
2957
- }) || 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
+ }
2958
3336
  showDetectedApps(interactiveTargets);
2959
3337
  selectedSources = await selectChatSources(interactiveTargets);
2960
3338
  interactiveMode = true;
@@ -2966,8 +3344,20 @@ async function main() {
2966
3344
  return;
2967
3345
  }
2968
3346
  if (!apiKey) {
2969
- printMissingKeyError();
2970
- 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
+ }
2971
3361
  }
2972
3362
  if (opts.installSync) {
2973
3363
  const auth2 = await validateKey(apiKey);
@@ -3090,6 +3480,7 @@ Nothing new to index.`);
3090
3480
  write(renderProgressSummary(success, failed, "indexed"));
3091
3481
  const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
3092
3482
  markIngested("codebase", fileHashes);
3483
+ addIndexedPath(absPath);
3093
3484
  if (!opts.quiet)
3094
3485
  printFailureSummary(results, memories);
3095
3486
  }
@@ -3221,6 +3612,24 @@ Nothing new to index.`);
3221
3612
  }
3222
3613
  }
3223
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
+ }
3224
3633
  if (interactiveMode) {
3225
3634
  selectedTargets = await selectMcpTargets(interactiveTargets);
3226
3635
  const shouldIndexCurrentCodebase = await confirmIndexCodebase();
@@ -3264,6 +3673,7 @@ Nothing new to index.`);
3264
3673
  write(renderProgressSummary(success, failed, "indexed"));
3265
3674
  const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
3266
3675
  markIngested("codebase", fileHashes);
3676
+ addIndexedPath(absPath);
3267
3677
  if (!opts.quiet)
3268
3678
  printFailureSummary(results, memories);
3269
3679
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conare",
3
- "version": "0.3.4",
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": {