iranti 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,10 +9,10 @@
9
9
 
10
10
  Iranti gives agents persistent, identity-based memory. Facts written by one agent are retrievable by any other agent through exact entity+key lookup. Iranti also supports hybrid search (lexical + vector) when exact keys are unknown. Memory persists across sessions and survives context window limits.
11
11
 
12
- **Latest release:** [`v0.2.16`](https://github.com/nfemmanuel/iranti/releases/tag/v0.2.16)
12
+ **Latest release:** [`v0.2.19`](https://github.com/nfemmanuel/iranti/releases/tag/v0.2.19)
13
13
  Published packages:
14
- - `iranti@0.2.16`
15
- - `@iranti/sdk@0.2.16`
14
+ - `iranti@0.2.19`
15
+ - `@iranti/sdk@0.2.19`
16
16
 
17
17
  ---
18
18
 
@@ -71,10 +71,8 @@ Iranti has now been rerun against a broader benchmark program covering 13 capabi
71
71
  ### Current Limits
72
72
 
73
73
  - **Search is not yet full semantic paraphrase retrieval.**
74
- - **Observe still performs better with hints than without them.**
75
- - Two product defects remain under review:
76
- - silent retrieval drop when `/` appears inside certain fact values
77
- - `user/main` noise from benchmark smoke artifacts
74
+ - **Observe still performs better on confidence ranking than on broad progress-fact discovery.**
75
+ - **Structured search is operational, but not yet broad semantic paraphrase retrieval.**
78
76
 
79
77
  ### Practical Position
80
78
 
@@ -214,6 +212,15 @@ iranti run --instance local --debug
214
212
  iranti upgrade --verbose
215
213
  ```
216
214
 
215
+ If you want to remove Iranti cleanly:
216
+
217
+ ```bash
218
+ iranti uninstall --dry-run
219
+ iranti uninstall --all --yes
220
+ ```
221
+
222
+ Default uninstall keeps runtime data and project bindings. `--all` removes discovered runtime roots plus project-local Iranti integrations.
223
+
217
224
  Advanced/manual path:
218
225
 
219
226
  ```bash
@@ -2352,6 +2352,592 @@ async function executeUpgradeTarget(target, context, options = {}) {
2352
2352
  }
2353
2353
  return { target, steps, verification };
2354
2354
  }
2355
+ function resolveUninstallScanRoots(args) {
2356
+ const explicit = getFlag(args, 'scan-root');
2357
+ const candidates = explicit
2358
+ ? explicit.split(',').map((value) => path_1.default.resolve(value.trim())).filter(Boolean)
2359
+ : [
2360
+ process.cwd(),
2361
+ path_1.default.join(os_1.default.homedir(), 'Documents', 'Projects'),
2362
+ ].filter((value, index, array) => array.indexOf(value) === index);
2363
+ return candidates.filter((candidate, index, array) => candidate.length > 0
2364
+ && fs_1.default.existsSync(candidate)
2365
+ && array.indexOf(candidate) === index);
2366
+ }
2367
+ function runtimeRootFromInstanceEnv(envFile) {
2368
+ const normalized = path_1.default.resolve(envFile);
2369
+ const parts = normalized.split(path_1.default.sep);
2370
+ const instancesIndex = parts.lastIndexOf('instances');
2371
+ if (instancesIndex <= 0)
2372
+ return null;
2373
+ return parts.slice(0, instancesIndex).join(path_1.default.sep);
2374
+ }
2375
+ async function discoverRuntimeRoots(root, projectArtifacts, scanRoots) {
2376
+ const discovered = new Map();
2377
+ const add = (candidate, source) => {
2378
+ if (!candidate)
2379
+ return;
2380
+ const resolved = path_1.default.resolve(candidate);
2381
+ if (!fs_1.default.existsSync(resolved))
2382
+ return;
2383
+ if (!discovered.has(resolved)) {
2384
+ discovered.set(resolved, { path: resolved, source });
2385
+ }
2386
+ };
2387
+ add(root, 'active-root');
2388
+ for (const artifact of projectArtifacts) {
2389
+ if (!artifact.bindingFile || !fs_1.default.existsSync(artifact.bindingFile))
2390
+ continue;
2391
+ try {
2392
+ const binding = await readEnvFile(artifact.bindingFile);
2393
+ add(runtimeRootFromInstanceEnv(binding.IRANTI_INSTANCE_ENV ?? ''), 'binding');
2394
+ }
2395
+ catch {
2396
+ continue;
2397
+ }
2398
+ }
2399
+ for (const scanRoot of scanRoots) {
2400
+ const queue = [scanRoot];
2401
+ while (queue.length > 0) {
2402
+ const current = queue.shift();
2403
+ let entries = [];
2404
+ try {
2405
+ entries = await promises_1.default.readdir(current, { withFileTypes: true });
2406
+ }
2407
+ catch {
2408
+ continue;
2409
+ }
2410
+ for (const entry of entries) {
2411
+ if (!entry.isDirectory())
2412
+ continue;
2413
+ if (shouldSkipUninstallScanDir(entry.name))
2414
+ continue;
2415
+ const candidate = path_1.default.join(current, entry.name);
2416
+ if ((entry.name === '.iranti' || entry.name === '.iranti-runtime')
2417
+ && (fs_1.default.existsSync(path_1.default.join(candidate, 'install.json')) || fs_1.default.existsSync(path_1.default.join(candidate, 'instances')))) {
2418
+ add(candidate, 'scan');
2419
+ continue;
2420
+ }
2421
+ queue.push(candidate);
2422
+ }
2423
+ }
2424
+ }
2425
+ return Array.from(discovered.values()).sort((a, b) => a.path.localeCompare(b.path));
2426
+ }
2427
+ async function collectUninstallProcesses(runtimeRoots, context) {
2428
+ const processes = new Map();
2429
+ for (const runtimeRoot of runtimeRoots) {
2430
+ const instances = await collectRuntimeInstanceSummaries(runtimeRoot.path);
2431
+ for (const instance of instances) {
2432
+ const pid = instance.runtime.state?.pid;
2433
+ if (!instance.runtime.running || !pid || pid === process.pid)
2434
+ continue;
2435
+ processes.set(pid, {
2436
+ pid,
2437
+ source: 'runtime',
2438
+ label: `instance:${instance.name}`,
2439
+ command: instance.runtime.state?.healthUrl,
2440
+ });
2441
+ }
2442
+ }
2443
+ const probe = process.platform === 'win32'
2444
+ ? runCommandCapture('powershell', [
2445
+ '-NoProfile',
2446
+ '-Command',
2447
+ 'Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress',
2448
+ ])
2449
+ : runCommandCapture('ps', ['-ax', '-o', 'pid=', '-o', 'command=']);
2450
+ if (probe.status === 0) {
2451
+ if (process.platform === 'win32') {
2452
+ try {
2453
+ const payload = JSON.parse(probe.stdout);
2454
+ const rows = Array.isArray(payload) ? payload : [payload];
2455
+ const needles = [
2456
+ context.packageRootPath.toLowerCase(),
2457
+ context.globalNpmRoot?.toLowerCase(),
2458
+ 'iranti mcp',
2459
+ 'iranti run',
2460
+ 'iranti-cli',
2461
+ 'iranti-mcp',
2462
+ 'claude-code-memory-hook',
2463
+ ].filter((value) => Boolean(value));
2464
+ for (const row of rows) {
2465
+ const pid = row.ProcessId;
2466
+ const command = row.CommandLine ?? '';
2467
+ if (!pid || pid === process.pid || !command)
2468
+ continue;
2469
+ const lower = command.toLowerCase();
2470
+ if (!needles.some((needle) => lower.includes(needle)))
2471
+ continue;
2472
+ processes.set(pid, {
2473
+ pid,
2474
+ source: 'process-scan',
2475
+ label: 'iranti-process',
2476
+ command,
2477
+ });
2478
+ }
2479
+ }
2480
+ catch {
2481
+ // best effort only
2482
+ }
2483
+ }
2484
+ else {
2485
+ const needles = [
2486
+ context.packageRootPath.toLowerCase(),
2487
+ context.globalNpmRoot?.toLowerCase(),
2488
+ 'iranti mcp',
2489
+ 'iranti run',
2490
+ 'iranti-cli',
2491
+ 'iranti-mcp',
2492
+ 'claude-code-memory-hook',
2493
+ ].filter((value) => Boolean(value));
2494
+ for (const line of probe.stdout.split(/\r?\n/)) {
2495
+ const match = line.trim().match(/^(\d+)\s+(.*)$/);
2496
+ if (!match)
2497
+ continue;
2498
+ const pid = Number.parseInt(match[1] ?? '', 10);
2499
+ const command = match[2] ?? '';
2500
+ if (!pid || pid === process.pid)
2501
+ continue;
2502
+ const lower = command.toLowerCase();
2503
+ if (!needles.some((needle) => lower.includes(needle)))
2504
+ continue;
2505
+ processes.set(pid, {
2506
+ pid,
2507
+ source: 'process-scan',
2508
+ label: 'iranti-process',
2509
+ command,
2510
+ });
2511
+ }
2512
+ }
2513
+ }
2514
+ return Array.from(processes.values()).sort((a, b) => a.pid - b.pid);
2515
+ }
2516
+ function detectCodexRegistration(name = 'iranti') {
2517
+ if (!hasCodexInstalled())
2518
+ return false;
2519
+ const proc = runCommandCapture('codex', ['mcp', 'get', name, '--json']);
2520
+ return proc.status === 0;
2521
+ }
2522
+ function removeIrantiMcpServerFromValue(value) {
2523
+ const mcpServers = value.mcpServers;
2524
+ if (!mcpServers || typeof mcpServers !== 'object' || Array.isArray(mcpServers))
2525
+ return value;
2526
+ const nextServers = { ...mcpServers };
2527
+ delete nextServers.iranti;
2528
+ if (Object.keys(nextServers).length === 0) {
2529
+ const next = { ...value };
2530
+ delete next.mcpServers;
2531
+ return Object.keys(next).length === 0 ? null : next;
2532
+ }
2533
+ return {
2534
+ ...value,
2535
+ mcpServers: nextServers,
2536
+ };
2537
+ }
2538
+ function removeIrantiClaudeHooksFromValue(value) {
2539
+ const hooks = isClaudeHooksObject(value.hooks) ? value.hooks : null;
2540
+ if (!hooks)
2541
+ return value;
2542
+ const nextHooks = { ...hooks };
2543
+ for (const event of ['SessionStart', 'UserPromptSubmit']) {
2544
+ const entries = hooks[event];
2545
+ if (!Array.isArray(entries))
2546
+ continue;
2547
+ const filtered = entries.filter((entry) => {
2548
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
2549
+ return true;
2550
+ if (isLegacyIrantiClaudeHookEntry(entry))
2551
+ return false;
2552
+ const structured = entry;
2553
+ const nestedHooks = Array.isArray(structured.hooks) ? structured.hooks : [];
2554
+ const remainingNested = nestedHooks.filter((hook) => {
2555
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook))
2556
+ return true;
2557
+ const command = typeof hook.command === 'string'
2558
+ ? String(hook.command)
2559
+ : '';
2560
+ return !command.includes('iranti claude-hook');
2561
+ });
2562
+ if (remainingNested.length !== nestedHooks.length) {
2563
+ if (remainingNested.length === 0) {
2564
+ return false;
2565
+ }
2566
+ structured.hooks = remainingNested;
2567
+ }
2568
+ return true;
2569
+ });
2570
+ if (filtered.length === 0) {
2571
+ delete nextHooks[event];
2572
+ }
2573
+ else {
2574
+ nextHooks[event] = filtered;
2575
+ }
2576
+ }
2577
+ const next = { ...value };
2578
+ if (Object.keys(nextHooks).length === 0) {
2579
+ delete next.hooks;
2580
+ }
2581
+ else {
2582
+ next.hooks = nextHooks;
2583
+ }
2584
+ return Object.keys(next).length === 0 ? null : next;
2585
+ }
2586
+ async function cleanupProjectArtifacts(artifacts) {
2587
+ const results = [];
2588
+ for (const artifact of artifacts) {
2589
+ if (artifact.bindingFile && fs_1.default.existsSync(artifact.bindingFile)) {
2590
+ await promises_1.default.rm(artifact.bindingFile, { force: true });
2591
+ results.push({
2592
+ label: 'project-binding',
2593
+ status: 'pass',
2594
+ detail: `Removed ${artifact.bindingFile}`,
2595
+ });
2596
+ }
2597
+ if (artifact.mcpFile && fs_1.default.existsSync(artifact.mcpFile)) {
2598
+ const parsed = readJsonFile(artifact.mcpFile);
2599
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
2600
+ const next = removeIrantiMcpServerFromValue(parsed);
2601
+ if (!next) {
2602
+ await promises_1.default.rm(artifact.mcpFile, { force: true });
2603
+ results.push({
2604
+ label: 'project-mcp',
2605
+ status: 'pass',
2606
+ detail: `Removed ${artifact.mcpFile}`,
2607
+ });
2608
+ }
2609
+ else {
2610
+ await writeText(artifact.mcpFile, `${JSON.stringify(next, null, 2)}\n`);
2611
+ results.push({
2612
+ label: 'project-mcp',
2613
+ status: 'pass',
2614
+ detail: `Removed Iranti MCP entry from ${artifact.mcpFile}`,
2615
+ });
2616
+ }
2617
+ }
2618
+ else {
2619
+ results.push({
2620
+ label: 'project-mcp',
2621
+ status: 'warn',
2622
+ detail: `Skipped unreadable JSON file ${artifact.mcpFile}`,
2623
+ });
2624
+ }
2625
+ }
2626
+ if (artifact.claudeSettingsFile && fs_1.default.existsSync(artifact.claudeSettingsFile)) {
2627
+ const parsed = readJsonFile(artifact.claudeSettingsFile);
2628
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
2629
+ const next = removeIrantiClaudeHooksFromValue(parsed);
2630
+ if (!next) {
2631
+ await promises_1.default.rm(artifact.claudeSettingsFile, { force: true });
2632
+ results.push({
2633
+ label: 'project-claude',
2634
+ status: 'pass',
2635
+ detail: `Removed ${artifact.claudeSettingsFile}`,
2636
+ });
2637
+ }
2638
+ else {
2639
+ await writeText(artifact.claudeSettingsFile, `${JSON.stringify(next, null, 2)}\n`);
2640
+ results.push({
2641
+ label: 'project-claude',
2642
+ status: 'pass',
2643
+ detail: `Removed Iranti Claude hooks from ${artifact.claudeSettingsFile}`,
2644
+ });
2645
+ }
2646
+ }
2647
+ else {
2648
+ results.push({
2649
+ label: 'project-claude',
2650
+ status: 'warn',
2651
+ detail: `Skipped unreadable JSON file ${artifact.claudeSettingsFile}`,
2652
+ });
2653
+ }
2654
+ }
2655
+ }
2656
+ return results;
2657
+ }
2658
+ async function runUninstallCommand(step) {
2659
+ const proc = runCommandCapture(step.executable, step.args, step.cwd);
2660
+ if (proc.status === 0) {
2661
+ return {
2662
+ label: step.label,
2663
+ status: 'pass',
2664
+ detail: `${step.display} completed successfully.`,
2665
+ };
2666
+ }
2667
+ return {
2668
+ label: step.label,
2669
+ status: 'warn',
2670
+ detail: `${step.display} exited with status ${proc.status ?? -1}: ${(proc.stderr || proc.stdout).trim() || 'unknown error'}`,
2671
+ };
2672
+ }
2673
+ async function stopUninstallProcesses(processes) {
2674
+ const results = [];
2675
+ for (const candidate of processes) {
2676
+ const stopped = await stopRuntimeProcess(candidate.pid, 5000);
2677
+ results.push({
2678
+ label: 'stop-process',
2679
+ status: stopped ? 'pass' : 'warn',
2680
+ detail: `${stopped ? 'Stopped' : 'Could not stop'} pid=${candidate.pid} (${candidate.label})`,
2681
+ });
2682
+ }
2683
+ return results;
2684
+ }
2685
+ function buildDetachedWindowsUninstallScript(options) {
2686
+ const lines = [
2687
+ `$parentPid = ${options.parentPid}`,
2688
+ 'while (Get-Process -Id $parentPid -ErrorAction SilentlyContinue) { Start-Sleep -Milliseconds 500 }',
2689
+ ];
2690
+ for (const pid of options.stopPids) {
2691
+ lines.push(`taskkill /PID ${pid} /T /F > $null 2>&1`);
2692
+ }
2693
+ if (options.removeCodex) {
2694
+ lines.push("$codexGet = Get-Command codex -ErrorAction SilentlyContinue");
2695
+ lines.push("if ($codexGet) { codex mcp get iranti --json > $null 2>&1; if ($LASTEXITCODE -eq 0) { codex mcp remove iranti > $null 2>&1 } }");
2696
+ }
2697
+ if (options.removeGlobalNpm) {
2698
+ lines.push('& npm uninstall -g iranti');
2699
+ }
2700
+ if (options.python) {
2701
+ const args = options.python.args.map((arg) => `'${escapeForSingleQuotedPowerShell(arg)}'`).join(', ');
2702
+ lines.push(`& '${escapeForSingleQuotedPowerShell(options.python.executable)}' @(${args})`);
2703
+ }
2704
+ for (const filePath of options.artifactFiles) {
2705
+ lines.push(`if (Test-Path -LiteralPath '${escapeForSingleQuotedPowerShell(filePath)}') { Remove-Item -LiteralPath '${escapeForSingleQuotedPowerShell(filePath)}' -Force }`);
2706
+ }
2707
+ for (const dirPath of options.runtimeRoots) {
2708
+ lines.push(`if (Test-Path -LiteralPath '${escapeForSingleQuotedPowerShell(dirPath)}') { Remove-Item -LiteralPath '${escapeForSingleQuotedPowerShell(dirPath)}' -Recurse -Force }`);
2709
+ }
2710
+ lines.push('exit 0');
2711
+ return lines.join('; ');
2712
+ }
2713
+ async function uninstallCommand(args) {
2714
+ const scope = normalizeScope(getFlag(args, 'scope'));
2715
+ const root = resolveInstallRoot(args, scope);
2716
+ const json = hasFlag(args, 'json');
2717
+ const dryRun = hasFlag(args, 'dry-run');
2718
+ const executeFlag = hasFlag(args, 'yes');
2719
+ const removeAll = hasFlag(args, 'all');
2720
+ const keepData = hasFlag(args, 'keep-data');
2721
+ const keepProjectBindings = hasFlag(args, 'keep-project-bindings');
2722
+ const scanRoots = resolveUninstallScanRoots(args);
2723
+ const context = detectUpgradeContext(args);
2724
+ const projectArtifacts = removeAll && !keepProjectBindings
2725
+ ? await discoverProjectArtifacts(scanRoots)
2726
+ : [];
2727
+ const runtimeRoots = await discoverRuntimeRoots(root, projectArtifacts, scanRoots);
2728
+ const processes = await collectUninstallProcesses(runtimeRoots, context);
2729
+ const codexRegistration = removeAll && !keepProjectBindings && detectCodexRegistration('iranti');
2730
+ const pythonCommand = context.python
2731
+ ? {
2732
+ ...context.python,
2733
+ label: 'python uninstall',
2734
+ display: `${context.python.executable}${context.python.args[0] === '-3' ? ' -3' : ''} -m pip uninstall -y iranti`,
2735
+ args: (context.python.args[0] === '-3' ? ['-3', '-m', 'pip'] : ['-m', 'pip']).concat(['uninstall', '-y', 'iranti']),
2736
+ }
2737
+ : null;
2738
+ const actions = {
2739
+ stopProcesses: processes.length > 0,
2740
+ removeGlobalNpm: context.globalNpmInstall,
2741
+ removePython: context.pythonVersion !== null && pythonCommand !== null,
2742
+ removeRuntimeRoots: removeAll && !keepData && runtimeRoots.length > 0,
2743
+ removeProjectBindings: removeAll && !keepProjectBindings && projectArtifacts.length > 0,
2744
+ removeCodexRegistration: codexRegistration,
2745
+ };
2746
+ let execute = executeFlag;
2747
+ let note = null;
2748
+ if (!execute && !dryRun && !json && process.stdin.isTTY && process.stdout.isTTY) {
2749
+ await withPromptSession(async (prompt) => {
2750
+ execute = await promptYesNo(prompt, 'Proceed with uninstall using the plan below?', false);
2751
+ });
2752
+ if (!execute) {
2753
+ note = 'Uninstall cancelled.';
2754
+ }
2755
+ }
2756
+ else if (!execute && !dryRun) {
2757
+ note = 'Run with --yes to execute the uninstall, or use --dry-run to inspect the plan safely.';
2758
+ }
2759
+ const plannedSteps = [];
2760
+ if (actions.stopProcesses)
2761
+ plannedSteps.push(`Stop ${processes.length} live Iranti process(es)`);
2762
+ if (actions.removeGlobalNpm)
2763
+ plannedSteps.push('Remove global npm install');
2764
+ if (actions.removePython)
2765
+ plannedSteps.push('Remove Python client');
2766
+ if (actions.removeCodexRegistration)
2767
+ plannedSteps.push('Remove Codex MCP registration');
2768
+ if (actions.removeProjectBindings)
2769
+ plannedSteps.push(`Clean ${projectArtifacts.length} project binding/integration surface(s)`);
2770
+ if (actions.removeRuntimeRoots)
2771
+ plannedSteps.push(`Delete ${runtimeRoots.length} runtime root(s)`);
2772
+ const actionLabel = execute ? 'uninstall' : dryRun ? 'dry-run' : 'inspect';
2773
+ const execution = [];
2774
+ const requiresDetachedWindowsSelfUninstall = process.platform === 'win32'
2775
+ && actions.removeGlobalNpm
2776
+ && context.runningFromGlobalNpmInstall
2777
+ && execute
2778
+ && !dryRun;
2779
+ if (execute && !dryRun) {
2780
+ if (requiresDetachedWindowsSelfUninstall) {
2781
+ const artifactFiles = projectArtifacts.flatMap((artifact) => [artifact.bindingFile, artifact.mcpFile, artifact.claudeSettingsFile]
2782
+ .filter((value) => Boolean(value)));
2783
+ const script = buildDetachedWindowsUninstallScript({
2784
+ parentPid: process.pid,
2785
+ stopPids: processes.map((candidate) => candidate.pid),
2786
+ removeCodex: actions.removeCodexRegistration,
2787
+ python: actions.removePython ? pythonCommand : null,
2788
+ removeGlobalNpm: actions.removeGlobalNpm,
2789
+ runtimeRoots: actions.removeRuntimeRoots ? runtimeRoots.map((entry) => entry.path) : [],
2790
+ artifactFiles,
2791
+ });
2792
+ const child = (0, child_process_1.spawn)('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], {
2793
+ detached: true,
2794
+ stdio: 'ignore',
2795
+ windowsHide: true,
2796
+ cwd: os_1.default.homedir(),
2797
+ env: process.env,
2798
+ });
2799
+ child.unref();
2800
+ execution.push({
2801
+ label: 'detached-uninstall',
2802
+ status: 'warn',
2803
+ detail: 'Scheduled detached uninstall because the current Windows CLI cannot remove its own live global npm install in place.',
2804
+ });
2805
+ note = 'Wait a few seconds, then open a new shell and verify `iranti` is gone from PATH.';
2806
+ }
2807
+ else {
2808
+ if (actions.stopProcesses) {
2809
+ execution.push(...await stopUninstallProcesses(processes));
2810
+ }
2811
+ if (actions.removeCodexRegistration) {
2812
+ const proc = runCommandCapture('codex', ['mcp', 'remove', 'iranti']);
2813
+ execution.push({
2814
+ label: 'codex-mcp',
2815
+ status: proc.status === 0 ? 'pass' : 'warn',
2816
+ detail: proc.status === 0
2817
+ ? 'Removed Codex MCP registration.'
2818
+ : `Could not remove Codex MCP registration: ${(proc.stderr || proc.stdout).trim() || 'unknown error'}`,
2819
+ });
2820
+ }
2821
+ if (actions.removeGlobalNpm) {
2822
+ execution.push(await runUninstallCommand({
2823
+ label: 'npm uninstall',
2824
+ display: 'npm uninstall -g iranti',
2825
+ executable: 'npm',
2826
+ args: ['uninstall', '-g', 'iranti'],
2827
+ cwd: context.packageRootPath,
2828
+ }));
2829
+ }
2830
+ if (actions.removePython && pythonCommand) {
2831
+ execution.push(await runUninstallCommand(pythonCommand));
2832
+ }
2833
+ if (actions.removeProjectBindings) {
2834
+ execution.push(...await cleanupProjectArtifacts(projectArtifacts));
2835
+ }
2836
+ if (actions.removeRuntimeRoots) {
2837
+ for (const runtimeRoot of runtimeRoots) {
2838
+ await promises_1.default.rm(runtimeRoot.path, { recursive: true, force: true });
2839
+ execution.push({
2840
+ label: 'runtime-root',
2841
+ status: 'pass',
2842
+ detail: `Removed ${runtimeRoot.path}`,
2843
+ });
2844
+ }
2845
+ }
2846
+ }
2847
+ }
2848
+ if (json) {
2849
+ console.log(JSON.stringify({
2850
+ currentVersion: context.currentVersion,
2851
+ runtimeRoot: root,
2852
+ scanRoots,
2853
+ removeAll,
2854
+ keepData,
2855
+ keepProjectBindings,
2856
+ install: {
2857
+ globalNpmVersion: context.globalNpmVersion,
2858
+ pythonVersion: context.pythonVersion,
2859
+ runningFromGlobalNpmInstall: context.runningFromGlobalNpmInstall,
2860
+ codexRegistration,
2861
+ },
2862
+ runtimeRoots,
2863
+ projectArtifacts,
2864
+ processes,
2865
+ actions,
2866
+ plan: plannedSteps,
2867
+ action: actionLabel,
2868
+ execution,
2869
+ note,
2870
+ }, null, 2));
2871
+ return;
2872
+ }
2873
+ console.log(sectionTitle('Iranti Uninstall'));
2874
+ console.log(` current_version ${context.currentVersion}`);
2875
+ console.log(` runtime_root ${root}`);
2876
+ console.log(` npm_global ${context.globalNpmVersion ?? paint('not installed', 'gray')}`);
2877
+ console.log(` python ${context.pythonVersion ?? paint('not installed', 'gray')}`);
2878
+ console.log(` codex_registration ${codexRegistration ? paint('yes', 'green') : paint('no', 'gray')}`);
2879
+ console.log(` remove_all ${removeAll ? paint('yes', 'yellow') : paint('no', 'gray')}`);
2880
+ console.log(` keep_data ${keepData ? paint('yes', 'yellow') : paint('no', 'gray')}`);
2881
+ console.log(` keep_project_bindings ${keepProjectBindings ? paint('yes', 'yellow') : paint('no', 'gray')}`);
2882
+ console.log('');
2883
+ console.log(` scan_roots ${scanRoots.length > 0 ? scanRoots.join(', ') : '(none)'}`);
2884
+ console.log(` live_processes ${processes.length}`);
2885
+ console.log(` project_artifacts ${projectArtifacts.length}`);
2886
+ console.log(` runtime_roots ${runtimeRoots.length}`);
2887
+ console.log('');
2888
+ if (plannedSteps.length > 0) {
2889
+ console.log(' plan');
2890
+ for (const step of plannedSteps) {
2891
+ console.log(` - ${step}`);
2892
+ }
2893
+ }
2894
+ else {
2895
+ console.log(' plan');
2896
+ console.log(' - Nothing to remove.');
2897
+ }
2898
+ if (processes.length > 0) {
2899
+ console.log('');
2900
+ console.log(' processes');
2901
+ for (const candidate of processes) {
2902
+ console.log(` - pid=${candidate.pid} ${candidate.label}${candidate.command ? ` :: ${truncateText(candidate.command, 120)}` : ''}`);
2903
+ }
2904
+ }
2905
+ if (projectArtifacts.length > 0) {
2906
+ console.log('');
2907
+ console.log(' project_artifacts');
2908
+ for (const artifact of projectArtifacts) {
2909
+ console.log(` - ${artifact.projectPath}`);
2910
+ if (artifact.bindingFile)
2911
+ console.log(` binding ${artifact.bindingFile}`);
2912
+ if (artifact.mcpFile)
2913
+ console.log(` mcp ${artifact.mcpFile}`);
2914
+ if (artifact.claudeSettingsFile)
2915
+ console.log(` claude ${artifact.claudeSettingsFile}`);
2916
+ }
2917
+ }
2918
+ if (runtimeRoots.length > 0) {
2919
+ console.log('');
2920
+ console.log(' runtime_roots');
2921
+ for (const runtimeRoot of runtimeRoots) {
2922
+ console.log(` - ${runtimeRoot.path} (${runtimeRoot.source})`);
2923
+ }
2924
+ }
2925
+ if (execution.length > 0) {
2926
+ console.log('');
2927
+ for (const result of execution) {
2928
+ const marker = result.status === 'pass'
2929
+ ? okLabel('PASS')
2930
+ : result.status === 'warn'
2931
+ ? warnLabel('WARN')
2932
+ : failLabel('FAIL');
2933
+ console.log(`${marker} ${result.detail}`);
2934
+ }
2935
+ }
2936
+ if (note) {
2937
+ console.log('');
2938
+ console.log(`${infoLabel()} ${note}`);
2939
+ }
2940
+ }
2355
2941
  async function listProviderKeysCommand(args) {
2356
2942
  const target = await resolveProviderKeyTarget(args);
2357
2943
  const currentProvider = normalizeProvider(target.env.LLM_PROVIDER ?? 'mock');
@@ -3962,6 +4548,96 @@ function findClaudeProjects(scanDir, recursive) {
3962
4548
  found.delete(scanDir);
3963
4549
  return Array.from(found).sort((a, b) => a.localeCompare(b));
3964
4550
  }
4551
+ function shouldSkipUninstallScanDir(name) {
4552
+ if (name.startsWith('.git'))
4553
+ return true;
4554
+ return shouldSkipRecursiveClaudeScanDir(name) || [
4555
+ '.venv',
4556
+ 'venv',
4557
+ ].includes(name);
4558
+ }
4559
+ function hasIrantiMcpServerConfig(value) {
4560
+ if (!value || typeof value !== 'object' || Array.isArray(value))
4561
+ return false;
4562
+ const record = value;
4563
+ const mcpServers = record.mcpServers;
4564
+ if (!mcpServers || typeof mcpServers !== 'object' || Array.isArray(mcpServers))
4565
+ return false;
4566
+ return Object.prototype.hasOwnProperty.call(mcpServers, 'iranti');
4567
+ }
4568
+ function hasIrantiClaudeHookSettings(value) {
4569
+ if (!value || typeof value !== 'object' || Array.isArray(value))
4570
+ return false;
4571
+ const record = value;
4572
+ const hooks = isClaudeHooksObject(record.hooks) ? record.hooks : null;
4573
+ if (!hooks)
4574
+ return false;
4575
+ for (const event of ['SessionStart', 'UserPromptSubmit']) {
4576
+ const entries = hooks[event];
4577
+ if (!Array.isArray(entries))
4578
+ continue;
4579
+ for (const entry of entries) {
4580
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
4581
+ continue;
4582
+ if (isLegacyIrantiClaudeHookEntry(entry))
4583
+ return true;
4584
+ const structured = entry;
4585
+ const nestedHooks = Array.isArray(structured.hooks) ? structured.hooks : [];
4586
+ if (nestedHooks.some((hook) => {
4587
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook))
4588
+ return false;
4589
+ const command = typeof hook.command === 'string'
4590
+ ? String(hook.command)
4591
+ : '';
4592
+ return command.includes('iranti claude-hook');
4593
+ })) {
4594
+ return true;
4595
+ }
4596
+ }
4597
+ }
4598
+ return false;
4599
+ }
4600
+ async function discoverProjectArtifacts(scanRoots) {
4601
+ const projects = new Map();
4602
+ for (const scanRoot of scanRoots) {
4603
+ if (!fs_1.default.existsSync(scanRoot))
4604
+ continue;
4605
+ const queue = [scanRoot];
4606
+ while (queue.length > 0) {
4607
+ const current = queue.shift();
4608
+ let entries = [];
4609
+ try {
4610
+ entries = await promises_1.default.readdir(current, { withFileTypes: true });
4611
+ }
4612
+ catch {
4613
+ continue;
4614
+ }
4615
+ const bindingFile = path_1.default.join(current, '.env.iranti');
4616
+ const mcpFile = path_1.default.join(current, '.mcp.json');
4617
+ const claudeSettingsFile = path_1.default.join(current, '.claude', 'settings.local.json');
4618
+ const artifact = { projectPath: current };
4619
+ if (fs_1.default.existsSync(bindingFile))
4620
+ artifact.bindingFile = bindingFile;
4621
+ if (fs_1.default.existsSync(mcpFile) && hasIrantiMcpServerConfig(readJsonFile(mcpFile))) {
4622
+ artifact.mcpFile = mcpFile;
4623
+ }
4624
+ if (fs_1.default.existsSync(claudeSettingsFile) && hasIrantiClaudeHookSettings(readJsonFile(claudeSettingsFile))) {
4625
+ artifact.claudeSettingsFile = claudeSettingsFile;
4626
+ }
4627
+ if (artifact.bindingFile || artifact.mcpFile || artifact.claudeSettingsFile) {
4628
+ projects.set(current, artifact);
4629
+ }
4630
+ for (const entry of entries) {
4631
+ if (!entry.isDirectory())
4632
+ continue;
4633
+ if (shouldSkipUninstallScanDir(entry.name))
4634
+ continue;
4635
+ queue.push(path_1.default.join(current, entry.name));
4636
+ }
4637
+ }
4638
+ }
4639
+ return Array.from(projects.values()).sort((a, b) => a.projectPath.localeCompare(b.projectPath));
4640
+ }
3965
4641
  async function claudeSetupCommand(args) {
3966
4642
  if (hasFlag(args, 'help')) {
3967
4643
  printClaudeSetupHelp();
@@ -4097,6 +4773,7 @@ function printHelp() {
4097
4773
  ['iranti doctor [--instance <name>] [--scope user|system] [--env <file>] [--json] [--debug]', 'Run environment and runtime diagnostics.'],
4098
4774
  ['iranti status [--scope user|system] [--json]', 'Show runtime roots, bindings, and known instances.'],
4099
4775
  ['iranti upgrade [--check] [--dry-run] [--yes] [--all] [--target auto|npm-global|npm-repo|python[,python]] [--json]', 'Check or run CLI/runtime/package upgrades.'],
4776
+ ['iranti uninstall [--dry-run] [--yes] [--all] [--keep-data] [--keep-project-bindings] [--scan-root <dir[,dir2]>] [--json]', 'Remove Iranti packages and, with --all, runtime data and project integrations.'],
4100
4777
  ['iranti handshake [--instance <name> | --project-env <file>] [--agent <id>] [--task <text>] [--recent <msg1||msg2>] [--recent-file <path>] [--json]', 'Manually inspect Attendant handshake output.'],
4101
4778
  ['iranti attend [message] [--instance <name> | --project-env <file>] [--agent <id>] [--context <text> | --context-file <path>] [--entity-hint <entity>] [--force] [--max-facts <n>] [--json]', 'Manually inspect turn-level memory injection decisions.'],
4102
4779
  ['iranti chat [--agent <agent-id>] [--provider <provider>] [--model <model>]', 'Open the local interactive chat shell.'],
@@ -4135,6 +4812,14 @@ function printSetupHelp() {
4135
4812
  console.log(' Use `--config <file>` to execute a saved setup plan.');
4136
4813
  console.log(' `--projects` and `--claude-code` apply to the non-interactive defaults flow.');
4137
4814
  }
4815
+ function printUninstallHelp() {
4816
+ console.log(sectionTitle('Uninstall Command'));
4817
+ console.log(` ${commandText('iranti uninstall [--scope user|system] [--root <path>] [--dry-run] [--yes] [--all] [--keep-data] [--keep-project-bindings] [--scan-root <dir[,dir2]>] [--json]')}`);
4818
+ console.log('');
4819
+ console.log(' Default mode removes installed packages and stops live Iranti processes, but keeps runtime data and project bindings.');
4820
+ console.log(' Add `--all` to also remove discovered runtime roots, `.env.iranti`, `.mcp.json` Iranti entries, and Claude hook settings.');
4821
+ console.log(' Use `--scan-root` to control where project bindings and isolated runtime roots are discovered.');
4822
+ }
4138
4823
  function printInstanceHelp() {
4139
4824
  console.log(sectionTitle('Instance Commands'));
4140
4825
  console.log(` ${commandText('iranti instance create <name> [--port 3001] [--db-url <url>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--scope user|system] [--root <path>]')}`);
@@ -4301,6 +4986,14 @@ async function main() {
4301
4986
  await upgradeCommand(args);
4302
4987
  return;
4303
4988
  }
4989
+ if (args.command === 'uninstall') {
4990
+ if (hasFlag(args, 'help')) {
4991
+ printUninstallHelp();
4992
+ return;
4993
+ }
4994
+ await uninstallCommand(args);
4995
+ return;
4996
+ }
4304
4997
  if (args.command === 'handshake') {
4305
4998
  await handshakeCommand(args);
4306
4999
  return;
@@ -144,7 +144,7 @@ async function main() {
144
144
  await ensureDefaultAgent(iranti);
145
145
  const server = new mcp_js_1.McpServer({
146
146
  name: 'iranti-mcp',
147
- version: '0.2.17',
147
+ version: '0.2.19',
148
148
  });
149
149
  server.registerTool('iranti_handshake', {
150
150
  description: `Initialize or refresh an agent's working-memory brief for the current task.
@@ -39,7 +39,7 @@ const INSTANCE_DIR = process.env.IRANTI_INSTANCE_DIR?.trim()
39
39
  const INSTANCE_RUNTIME_FILE = process.env.IRANTI_INSTANCE_RUNTIME_FILE?.trim()
40
40
  || (INSTANCE_DIR ? (0, runtimeLifecycle_1.runtimeFileForInstance)(INSTANCE_DIR) : null);
41
41
  const INSTANCE_NAME = process.env.IRANTI_INSTANCE_NAME?.trim() || (INSTANCE_DIR ? path_1.default.basename(INSTANCE_DIR) : 'adhoc');
42
- const VERSION = '0.2.17';
42
+ const VERSION = '0.2.19';
43
43
  try {
44
44
  fs_1.default.mkdirSync(path_1.default.dirname(REQUEST_LOG_FILE), { recursive: true });
45
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iranti",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "description": "Memory infrastructure for multi-agent AI systems",
5
5
  "main": "dist/src/sdk/index.js",
6
6
  "files": [