hippo-memory 1.11.1 → 1.11.3
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 +71 -306
- package/dist/api.d.ts +132 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +396 -4
- package/dist/api.js.map +1 -1
- package/dist/cli.js +235 -381
- package/dist/cli.js.map +1 -1
- package/dist/dedupe.d.ts +34 -0
- package/dist/dedupe.d.ts.map +1 -0
- package/dist/dedupe.js +61 -0
- package/dist/dedupe.js.map +1 -0
- package/dist/hooks.d.ts +76 -18
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +225 -24
- package/dist/hooks.js.map +1 -1
- package/dist/src/api.js +396 -4
- package/dist/src/api.js.map +1 -1
- package/dist/src/cli.js +235 -381
- package/dist/src/cli.js.map +1 -1
- package/dist/src/dedupe.js +61 -0
- package/dist/src/dedupe.js.map +1 -0
- package/dist/src/hooks.js +225 -24
- package/dist/src/hooks.js.map +1 -1
- package/dist/src/version.js +7 -5
- package/dist/src/version.js.map +1 -1
- package/dist/version.d.ts +6 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +7 -5
- package/dist/version.js.map +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -30,12 +30,12 @@ import * as fs from 'fs';
|
|
|
30
30
|
import * as os from 'os';
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { execFileSync, execSync, spawn } from 'child_process';
|
|
33
|
-
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, } from './hooks.js';
|
|
34
|
-
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence,
|
|
33
|
+
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
34
|
+
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, computeSchemaFit, Layer, DECISION_HALF_LIFE_DAYS, } from './memory.js';
|
|
35
35
|
import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, RECALL_DEFAULT_DENY_SCOPES, } from './store.js';
|
|
36
|
-
import { markRetrieved,
|
|
36
|
+
import { markRetrieved, hybridSearch, physicsSearch, explainMatch, textOverlap } from './search.js';
|
|
37
37
|
import { renderTraceContent, parseSteps } from './trace.js';
|
|
38
|
-
import {
|
|
38
|
+
import { deduplicateStore } from './dedupe.js';
|
|
39
39
|
import { isEmbeddingAvailable, embedAll, embedMemory, loadEmbeddingIndex, resolveEmbeddingModel, } from './embeddings.js';
|
|
40
40
|
import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
41
41
|
import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
@@ -46,7 +46,7 @@ import { rowToGoal } from './goals.js';
|
|
|
46
46
|
import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
|
|
47
47
|
import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
|
|
48
48
|
import { extractPathTags } from './path-context.js';
|
|
49
|
-
import { detectScope
|
|
49
|
+
import { detectScope } from './scope.js';
|
|
50
50
|
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
51
51
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
52
52
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
|
|
@@ -391,10 +391,10 @@ function autoInstallHooks(quiet) {
|
|
|
391
391
|
installed.add(targetPath);
|
|
392
392
|
console.log(` Auto-installed ${hook} hook in ${hookDef.file}`);
|
|
393
393
|
}
|
|
394
|
-
// For
|
|
395
|
-
// Keeps `hippo init` in lockstep with `hippo hook install
|
|
396
|
-
//
|
|
397
|
-
if (hook === 'claude-code'
|
|
394
|
+
// For Claude Code, also install SessionEnd+SessionStart entries in its
|
|
395
|
+
// settings.json. Keeps `hippo init` in lockstep with `hippo hook install
|
|
396
|
+
// claude-code` and `hippo setup`.
|
|
397
|
+
if (hook === 'claude-code') {
|
|
398
398
|
const result = installJsonHooks(hook);
|
|
399
399
|
if (result.installedSessionEnd) {
|
|
400
400
|
console.log(` Auto-installed hippo session-end SessionEnd hook in ${hook} settings`);
|
|
@@ -415,6 +415,21 @@ function autoInstallHooks(quiet) {
|
|
|
415
415
|
console.log(` Migrated legacy SessionEnd entry to the new detached form`);
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
|
+
else if (hook === 'opencode') {
|
|
419
|
+
// opencode uses a TS plugin, not Claude Code's JSON-hook schema.
|
|
420
|
+
// See OPENCODE_PLUGIN_SOURCE in src/hooks.ts for the plugin file
|
|
421
|
+
// content + design rationale.
|
|
422
|
+
const result = installOpencodePlugin();
|
|
423
|
+
if (result.installed) {
|
|
424
|
+
console.log(` Auto-installed hippo opencode plugin -> ${result.pluginPath}`);
|
|
425
|
+
}
|
|
426
|
+
if (result.migratedLegacyHooks) {
|
|
427
|
+
console.log(` Removed legacy Claude Code-style hooks block from opencode.json — opencode can now launch`);
|
|
428
|
+
}
|
|
429
|
+
if (result.jsonRepairFailed) {
|
|
430
|
+
console.log(` WARNING: opencode.json is unparseable; legacy hooks block could not be auto-removed. Fix the file manually.`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
418
433
|
}
|
|
419
434
|
}
|
|
420
435
|
/**
|
|
@@ -1773,49 +1788,6 @@ function learnFromMemoryMd(hippoRoot) {
|
|
|
1773
1788
|
}
|
|
1774
1789
|
return imported;
|
|
1775
1790
|
}
|
|
1776
|
-
function deduplicateStore(hippoRoot, options = {}) {
|
|
1777
|
-
const threshold = options.threshold ?? 0.7;
|
|
1778
|
-
const dryRun = options.dryRun ?? false;
|
|
1779
|
-
const entries = loadAllEntries(hippoRoot);
|
|
1780
|
-
// Sort by strength desc, then retrieval count, so we keep the most valuable copy
|
|
1781
|
-
entries.sort((a, b) => {
|
|
1782
|
-
const sDiff = (b.strength ?? 0) - (a.strength ?? 0);
|
|
1783
|
-
if (Math.abs(sDiff) > 0.01)
|
|
1784
|
-
return sDiff;
|
|
1785
|
-
return (b.retrieval_count ?? 0) - (a.retrieval_count ?? 0);
|
|
1786
|
-
});
|
|
1787
|
-
const removed = new Set();
|
|
1788
|
-
const pairs = [];
|
|
1789
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1790
|
-
if (removed.has(entries[i].id))
|
|
1791
|
-
continue;
|
|
1792
|
-
for (let j = i + 1; j < entries.length; j++) {
|
|
1793
|
-
if (removed.has(entries[j].id))
|
|
1794
|
-
continue;
|
|
1795
|
-
const similarity = textOverlap(entries[i].content, entries[j].content);
|
|
1796
|
-
if (similarity <= threshold)
|
|
1797
|
-
continue;
|
|
1798
|
-
removed.add(entries[j].id);
|
|
1799
|
-
pairs.push({
|
|
1800
|
-
kept: entries[i].id,
|
|
1801
|
-
keptContent: entries[i].content,
|
|
1802
|
-
keptLayer: entries[i].layer,
|
|
1803
|
-
keptStrength: entries[i].strength ?? 0,
|
|
1804
|
-
removed: entries[j].id,
|
|
1805
|
-
removedContent: entries[j].content,
|
|
1806
|
-
removedLayer: entries[j].layer,
|
|
1807
|
-
removedStrength: entries[j].strength ?? 0,
|
|
1808
|
-
similarity,
|
|
1809
|
-
});
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
if (!dryRun) {
|
|
1813
|
-
for (const id of removed) {
|
|
1814
|
-
deleteEntry(hippoRoot, id);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
return { removed: removed.size, pairs };
|
|
1818
|
-
}
|
|
1819
1791
|
function cmdDedup(hippoRoot, flags) {
|
|
1820
1792
|
requireInit(hippoRoot);
|
|
1821
1793
|
const dryRun = Boolean(flags['dry-run']);
|
|
@@ -1910,9 +1882,55 @@ async function cmdSleep(hippoRoot, flags) {
|
|
|
1910
1882
|
restoreStdout();
|
|
1911
1883
|
}
|
|
1912
1884
|
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Render an api.sleep result as console output, byte-identical to the
|
|
1887
|
+
* pre-extraction inline implementation in cmdSleepCore.
|
|
1888
|
+
*/
|
|
1889
|
+
function renderSleepResult(result) {
|
|
1890
|
+
console.log(`Running consolidation${result.dryRun ? ' (dry run)' : ''}...`);
|
|
1891
|
+
console.log(`\nResults:`);
|
|
1892
|
+
console.log(` Active memories: ${result.active}`);
|
|
1893
|
+
console.log(` Removed (decayed): ${result.removed}`);
|
|
1894
|
+
console.log(` Merged episodic: ${result.mergedEpisodic}`);
|
|
1895
|
+
console.log(` New semantic: ${result.newSemantic}`);
|
|
1896
|
+
if (result.details && result.details.length > 0) {
|
|
1897
|
+
console.log('\nDetails:');
|
|
1898
|
+
for (const d of result.details) {
|
|
1899
|
+
console.log(d);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (result.dryRun)
|
|
1903
|
+
console.log('\n(dry run - nothing written)');
|
|
1904
|
+
if (result.deduped && result.deduped.removed > 0) {
|
|
1905
|
+
const { removed, semDups, epiDups, crossDups } = result.deduped;
|
|
1906
|
+
const parts = [];
|
|
1907
|
+
if (semDups > 0)
|
|
1908
|
+
parts.push(`${semDups} redundant semantic patterns`);
|
|
1909
|
+
if (epiDups > 0)
|
|
1910
|
+
parts.push(`${epiDups} duplicate episodic lessons`);
|
|
1911
|
+
if (crossDups > 0)
|
|
1912
|
+
parts.push(`${crossDups} cross-layer duplicates`);
|
|
1913
|
+
console.log(`\nDeduped ${removed} duplicates (${parts.join(', ')}). Kept stronger copies.`);
|
|
1914
|
+
}
|
|
1915
|
+
if (result.audit) {
|
|
1916
|
+
if (result.audit.errorsRemoved > 0) {
|
|
1917
|
+
console.log(`\nAudit: removed ${result.audit.errorsRemoved} junk memories (too short/empty).`);
|
|
1918
|
+
}
|
|
1919
|
+
if (result.audit.warningCount > 0) {
|
|
1920
|
+
console.log(`Audit: ${result.audit.warningCount} low-quality memories detected (run \`hippo audit\` for details).`);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
if (result.shared !== undefined && result.shared > 0) {
|
|
1924
|
+
console.log(`\nAuto-shared ${result.shared} high-value memories to global store.`);
|
|
1925
|
+
}
|
|
1926
|
+
if (result.ambient) {
|
|
1927
|
+
console.log(`\n${renderAmbientSummary(result.ambient)}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1913
1930
|
async function cmdSleepCore(hippoRoot, flags) {
|
|
1914
1931
|
requireInit(hippoRoot);
|
|
1915
|
-
// Auto-learn from git
|
|
1932
|
+
// Phase 1: Auto-learn from git + MEMORY.md (CLI-only, uses process.cwd() / os.homedir()).
|
|
1933
|
+
// Stays in cli.ts; api.sleep covers Phase 2-6 only.
|
|
1916
1934
|
if (!flags['no-learn']) {
|
|
1917
1935
|
const config = loadConfig(hippoRoot);
|
|
1918
1936
|
if (config.autoLearnOnSleep && isGitRepo(process.cwd())) {
|
|
@@ -1925,78 +1943,17 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
1925
1943
|
if (memImported > 0)
|
|
1926
1944
|
console.log(`Imported ${memImported} memories from Claude Code MEMORY.md files.`);
|
|
1927
1945
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
console.log(d);
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
if (dryRun)
|
|
1943
|
-
console.log('\n(dry run - nothing written)');
|
|
1944
|
-
// Auto-dedup after consolidation (unless dry-run)
|
|
1945
|
-
if (!dryRun) {
|
|
1946
|
-
const dedupResult = deduplicateStore(hippoRoot);
|
|
1947
|
-
if (dedupResult.removed > 0) {
|
|
1948
|
-
const semDups = dedupResult.pairs.filter(p => p.keptLayer === 'semantic' && p.removedLayer === 'semantic').length;
|
|
1949
|
-
const epiDups = dedupResult.pairs.filter(p => p.keptLayer === 'episodic' && p.removedLayer === 'episodic').length;
|
|
1950
|
-
const crossDups = dedupResult.pairs.filter(p => p.keptLayer !== p.removedLayer).length;
|
|
1951
|
-
const parts = [];
|
|
1952
|
-
if (semDups > 0)
|
|
1953
|
-
parts.push(`${semDups} redundant semantic patterns`);
|
|
1954
|
-
if (epiDups > 0)
|
|
1955
|
-
parts.push(`${epiDups} duplicate episodic lessons`);
|
|
1956
|
-
if (crossDups > 0)
|
|
1957
|
-
parts.push(`${crossDups} cross-layer duplicates`);
|
|
1958
|
-
console.log(`\nDeduped ${dedupResult.removed} duplicates (${parts.join(', ')}). Kept stronger copies.`);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
// Quality audit — remove junk, report warnings
|
|
1962
|
-
if (!dryRun) {
|
|
1963
|
-
const allEntries = loadAllEntries(hippoRoot);
|
|
1964
|
-
const audit = auditMemories(allEntries);
|
|
1965
|
-
if (audit.issues.length > 0) {
|
|
1966
|
-
const errors = audit.issues.filter(i => i.severity === 'error');
|
|
1967
|
-
const warnings = audit.issues.filter(i => i.severity === 'warning');
|
|
1968
|
-
if (errors.length > 0) {
|
|
1969
|
-
for (const issue of errors) {
|
|
1970
|
-
deleteEntry(hippoRoot, issue.memoryId);
|
|
1971
|
-
}
|
|
1972
|
-
console.log(`\nAudit: removed ${errors.length} junk memories (too short/empty).`);
|
|
1973
|
-
}
|
|
1974
|
-
if (warnings.length > 0) {
|
|
1975
|
-
console.log(`Audit: ${warnings.length} low-quality memories detected (run \`hippo audit\` for details).`);
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
// Auto-share high-transfer-score memories to global (unless --no-share or dry-run)
|
|
1980
|
-
if (!dryRun && !flags['no-share']) {
|
|
1981
|
-
const sleepConfig = loadConfig(hippoRoot);
|
|
1982
|
-
if (sleepConfig.autoShareOnSleep) {
|
|
1983
|
-
const shared = autoShare(hippoRoot, { minScore: 0.6 });
|
|
1984
|
-
if (shared.length > 0) {
|
|
1985
|
-
console.log(`\nAuto-shared ${shared.length} high-value memories to global store.`);
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
// Post-sleep ambient state summary
|
|
1990
|
-
if (!dryRun) {
|
|
1991
|
-
const postSleepConfig = loadConfig(hippoRoot);
|
|
1992
|
-
if (postSleepConfig.ambient.enabled) {
|
|
1993
|
-
const postSleepEntries = loadAllEntries(hippoRoot).filter(e => !e.superseded_by);
|
|
1994
|
-
if (postSleepEntries.length > 0) {
|
|
1995
|
-
const ambientState = computeAmbientState(postSleepEntries);
|
|
1996
|
-
console.log(`\n${renderAmbientSummary(ambientState)}`);
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
1946
|
+
// Phase 2-6: Pure-storage pipeline (consolidate + dedup + audit + share + ambient).
|
|
1947
|
+
const ctx = {
|
|
1948
|
+
hippoRoot,
|
|
1949
|
+
tenantId: resolveTenantId({}),
|
|
1950
|
+
actor: 'cli',
|
|
1951
|
+
};
|
|
1952
|
+
const result = await api.sleep(ctx, {
|
|
1953
|
+
dryRun: Boolean(flags['dry-run']),
|
|
1954
|
+
noShare: Boolean(flags['no-share']),
|
|
1955
|
+
});
|
|
1956
|
+
renderSleepResult(result);
|
|
2000
1957
|
}
|
|
2001
1958
|
/**
|
|
2002
1959
|
* Print the contents of the SessionEnd sleep log to stdout, then clear it.
|
|
@@ -2368,21 +2325,28 @@ function cmdOutcome(hippoRoot, flags) {
|
|
|
2368
2325
|
console.error('Specify --good or --bad');
|
|
2369
2326
|
process.exit(1);
|
|
2370
2327
|
}
|
|
2328
|
+
// Behavior fix (v1.11.3): cmdOutcome used to bypass api.outcome and do its
|
|
2329
|
+
// own readEntry/writeEntry inline, which silently skipped the audit_log
|
|
2330
|
+
// emission that the MCP outcome path already has via api.outcome. T6
|
|
2331
|
+
// rewires through api.outcome so every successful CLI 'outcome' call now
|
|
2332
|
+
// writes one audit_log row per affected id, matching MCP parity.
|
|
2333
|
+
const ctx = {
|
|
2334
|
+
hippoRoot,
|
|
2335
|
+
tenantId: resolveTenantId({}),
|
|
2336
|
+
actor: 'cli',
|
|
2337
|
+
};
|
|
2371
2338
|
const specificId = flags['id'] ? String(flags['id']) : null;
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
console.log('No recent recall to apply outcome to. Use --id <id> to target a specific memory.');
|
|
2376
|
-
return;
|
|
2339
|
+
let updated;
|
|
2340
|
+
if (specificId) {
|
|
2341
|
+
updated = api.outcome(ctx, [specificId], good).applied;
|
|
2377
2342
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
updated++;
|
|
2343
|
+
else {
|
|
2344
|
+
const r = api.outcomeForLastRecall(ctx, good);
|
|
2345
|
+
if (r.ids.length === 0) {
|
|
2346
|
+
console.log('No recent recall to apply outcome to. Use --id <id> to target a specific memory.');
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
updated = r.applied;
|
|
2386
2350
|
}
|
|
2387
2351
|
console.log(`Applied ${good ? 'positive' : 'negative'} outcome to ${updated} memor${updated === 1 ? 'y' : 'ies'}`);
|
|
2388
2352
|
}
|
|
@@ -2912,226 +2876,59 @@ function cmdCurrent(hippoRoot, args, flags) {
|
|
|
2912
2876
|
async function cmdContext(hippoRoot, args, flags) {
|
|
2913
2877
|
// --pinned-only fires on every UserPromptSubmit — including in directories
|
|
2914
2878
|
// that don't have a local .hippo. Skip requireInit for that path and fall
|
|
2915
|
-
// back to global-only
|
|
2879
|
+
// back to global-only inside api.getContext. The non-pinned path still
|
|
2880
|
+
// requires init (handled by CLI for user-friendly error messaging).
|
|
2916
2881
|
const pinnedOnly = flags['pinned-only'] === true;
|
|
2917
|
-
const hasLocal = isInitialized(hippoRoot);
|
|
2918
2882
|
if (!pinnedOnly) {
|
|
2919
2883
|
requireInit(hippoRoot);
|
|
2920
2884
|
}
|
|
2921
2885
|
const budget = parseInt(String(flags['budget'] ?? '1500'), 10);
|
|
2922
|
-
const limit = parseLimitFlag(flags['limit']);
|
|
2923
|
-
const includeRecent = parseCountFlag(flags['include-recent']);
|
|
2924
|
-
const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
2925
|
-
const ctxActiveScope = ctxExplicitScope || detectScope();
|
|
2926
|
-
// If budget is 0, skip entirely (zero token cost)
|
|
2927
2886
|
if (budget <= 0)
|
|
2928
2887
|
return;
|
|
2929
|
-
//
|
|
2888
|
+
// Resolve query: explicit args, --auto (git diff via CLI-side helper), or
|
|
2889
|
+
// fall through to api.getContext's '*' fallback. api.getContext is host-
|
|
2890
|
+
// agnostic so the auto-detect (which shells out to git) stays CLI-side.
|
|
2930
2891
|
let query = args.join(' ').trim();
|
|
2931
2892
|
if (!query && flags['auto']) {
|
|
2932
2893
|
query = autoDetectContext();
|
|
2933
2894
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
const
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
: null;
|
|
2959
|
-
const recentSessionEvents = hasLocal && activeSnapshot?.session_id
|
|
2960
|
-
? listSessionEvents(hippoRoot, resolveTenantId({}), { session_id: activeSnapshot.session_id, limit: 5 })
|
|
2961
|
-
: [];
|
|
2962
|
-
if (localEntries.length === 0 && globalEntries.length === 0 && !activeSnapshot && !sessionHandoff && recentSessionEvents.length === 0) {
|
|
2963
|
-
return;
|
|
2964
|
-
}
|
|
2965
|
-
// --pinned-only: restrict to pinned entries only. Used by the Claude Code
|
|
2966
|
-
// UserPromptSubmit hook so invariants stay in context every turn.
|
|
2967
|
-
// (pinnedOnly and hasLocal are declared at the top of this function.)
|
|
2968
|
-
if (pinnedOnly) {
|
|
2969
|
-
// loadConfig is safe even when local isn't initialized — it returns defaults.
|
|
2970
|
-
const pinnedCfg = loadConfig(hippoRoot);
|
|
2971
|
-
if (!pinnedCfg.pinnedInject.enabled)
|
|
2972
|
-
return; // user disabled via config
|
|
2973
|
-
// Effective budget: explicit --budget wins over config.
|
|
2974
|
-
const effBudget = flags['budget'] !== undefined ? budget : pinnedCfg.pinnedInject.budget;
|
|
2975
|
-
const nowP = new Date();
|
|
2976
|
-
const selectedIds = new Set();
|
|
2977
|
-
let usedP = 0;
|
|
2978
|
-
if (includeRecent > 0) {
|
|
2979
|
-
const recent = [
|
|
2980
|
-
...localEntries.map((entry) => ({ entry, isGlobal: false })),
|
|
2981
|
-
...globalEntries.map((entry) => ({ entry, isGlobal: true })),
|
|
2982
|
-
]
|
|
2983
|
-
.sort((a, b) => {
|
|
2984
|
-
const byCreated = Date.parse(b.entry.created) - Date.parse(a.entry.created);
|
|
2985
|
-
return byCreated !== 0 ? byCreated : b.entry.id.localeCompare(a.entry.id);
|
|
2986
|
-
})
|
|
2987
|
-
.slice(0, includeRecent)
|
|
2988
|
-
.map(({ entry, isGlobal }) => ({
|
|
2989
|
-
entry,
|
|
2990
|
-
score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1),
|
|
2991
|
-
tokens: estimateTokens(entry.content),
|
|
2992
|
-
isGlobal,
|
|
2993
|
-
}));
|
|
2994
|
-
for (const r of recent) {
|
|
2995
|
-
if (selectedIds.has(r.entry.id))
|
|
2996
|
-
continue;
|
|
2997
|
-
if (usedP + r.tokens > effBudget)
|
|
2998
|
-
continue;
|
|
2999
|
-
selectedItems.push(r);
|
|
3000
|
-
selectedIds.add(r.entry.id);
|
|
3001
|
-
usedP += r.tokens;
|
|
3002
|
-
}
|
|
3003
|
-
}
|
|
3004
|
-
const pinnedLocal = localEntries.filter((e) => e.pinned);
|
|
3005
|
-
const pinnedGlobal = globalEntries.filter((e) => e.pinned);
|
|
3006
|
-
if (pinnedLocal.length === 0 && pinnedGlobal.length === 0 && selectedItems.length === 0)
|
|
3007
|
-
return; // zero output
|
|
3008
|
-
const rankedPinned = [
|
|
3009
|
-
...pinnedLocal.map((e) => ({ entry: e, isGlobal: false })),
|
|
3010
|
-
...pinnedGlobal.map((e) => ({ entry: e, isGlobal: true })),
|
|
3011
|
-
]
|
|
3012
|
-
.map(({ entry, isGlobal }) => {
|
|
3013
|
-
const scopeSig = scopeMatch(entry.tags, ctxActiveScope);
|
|
3014
|
-
const sBst = scopeSig === 1 ? 1.5 : scopeSig === -1 ? 0.5 : 1.0;
|
|
3015
|
-
return {
|
|
3016
|
-
entry,
|
|
3017
|
-
score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1) * sBst,
|
|
3018
|
-
tokens: estimateTokens(entry.content),
|
|
3019
|
-
isGlobal,
|
|
3020
|
-
};
|
|
3021
|
-
})
|
|
3022
|
-
.sort((a, b) => b.score - a.score);
|
|
3023
|
-
for (const r of rankedPinned) {
|
|
3024
|
-
if (selectedIds.has(r.entry.id))
|
|
3025
|
-
continue;
|
|
3026
|
-
if (usedP + r.tokens > effBudget)
|
|
3027
|
-
continue;
|
|
3028
|
-
selectedItems.push(r);
|
|
3029
|
-
selectedIds.add(r.entry.id);
|
|
3030
|
-
usedP += r.tokens;
|
|
3031
|
-
}
|
|
3032
|
-
totalTokens = usedP;
|
|
3033
|
-
}
|
|
3034
|
-
else if (query === '*') {
|
|
3035
|
-
// No query: return strongest memories by strength, up to budget
|
|
3036
|
-
const now = new Date();
|
|
3037
|
-
const localRanked = localEntries
|
|
3038
|
-
.map((e) => ({
|
|
3039
|
-
entry: e,
|
|
3040
|
-
score: calculateStrength(e, now),
|
|
3041
|
-
tokens: estimateTokens(e.content),
|
|
3042
|
-
isGlobal: false,
|
|
3043
|
-
}))
|
|
3044
|
-
.sort((a, b) => b.score - a.score);
|
|
3045
|
-
const globalRanked = globalEntries
|
|
3046
|
-
.map((e) => ({
|
|
3047
|
-
entry: e,
|
|
3048
|
-
score: calculateStrength(e, now) * (1 / 1.2), // global slightly lower
|
|
3049
|
-
tokens: estimateTokens(e.content),
|
|
3050
|
-
isGlobal: true,
|
|
3051
|
-
}))
|
|
3052
|
-
.sort((a, b) => b.score - a.score);
|
|
3053
|
-
const combined = [...localRanked, ...globalRanked].sort((a, b) => b.score - a.score);
|
|
3054
|
-
let used = 0;
|
|
3055
|
-
for (const r of combined) {
|
|
3056
|
-
if (used + r.tokens > budget)
|
|
3057
|
-
continue;
|
|
3058
|
-
selectedItems.push(r);
|
|
3059
|
-
used += r.tokens;
|
|
3060
|
-
}
|
|
3061
|
-
totalTokens = used;
|
|
3062
|
-
}
|
|
3063
|
-
else {
|
|
3064
|
-
let results;
|
|
3065
|
-
if (hasGlobal) {
|
|
3066
|
-
const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget, scope: ctxActiveScope, tenantId });
|
|
3067
|
-
const localIndex = loadIndex(hippoRoot);
|
|
3068
|
-
results = merged.map((r) => ({
|
|
3069
|
-
entry: r.entry,
|
|
3070
|
-
score: r.score,
|
|
3071
|
-
tokens: r.tokens,
|
|
3072
|
-
isGlobal: !localIndex.entries[r.entry.id],
|
|
3073
|
-
}));
|
|
3074
|
-
}
|
|
3075
|
-
else {
|
|
3076
|
-
const ctxConfig = loadConfig(hippoRoot);
|
|
3077
|
-
const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
|
|
3078
|
-
const ctxResults = usePhysicsCtx
|
|
3079
|
-
? await physicsSearch(query, localEntries, { budget, hippoRoot, physicsConfig: ctxConfig.physics, scope: ctxActiveScope })
|
|
3080
|
-
: await hybridSearch(query, localEntries, { budget, hippoRoot, scope: ctxActiveScope });
|
|
3081
|
-
results = ctxResults.map((r) => ({
|
|
3082
|
-
entry: r.entry,
|
|
3083
|
-
score: r.score,
|
|
3084
|
-
tokens: r.tokens,
|
|
3085
|
-
isGlobal: false,
|
|
3086
|
-
}));
|
|
3087
|
-
}
|
|
3088
|
-
selectedItems = results;
|
|
3089
|
-
totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
3090
|
-
// A5 H4: emit recall audit event for context-mode searches. The recall
|
|
3091
|
-
// handler emits one of these per `hippo recall` invocation; context mode
|
|
3092
|
-
// is the same surface (search → user) and must leave the same audit trail.
|
|
3093
|
-
// Skip pinned-only and '*' fallback (handled in branches above which never
|
|
3094
|
-
// hit the search engines).
|
|
3095
|
-
const ctxRecallMetadata = {
|
|
3096
|
-
query: query.slice(0, 200),
|
|
3097
|
-
results: selectedItems.length,
|
|
3098
|
-
mode: 'context',
|
|
3099
|
-
};
|
|
3100
|
-
if (hasLocal)
|
|
3101
|
-
emitCliAudit(hippoRoot, 'recall', undefined, ctxRecallMetadata);
|
|
3102
|
-
if (hasGlobal)
|
|
3103
|
-
emitCliAudit(globalRoot, 'recall', undefined, ctxRecallMetadata);
|
|
3104
|
-
}
|
|
3105
|
-
if (limit < selectedItems.length) {
|
|
3106
|
-
selectedItems = selectedItems.slice(0, limit);
|
|
3107
|
-
totalTokens = selectedItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
3108
|
-
}
|
|
3109
|
-
if (selectedItems.length === 0 && !activeSnapshot && !sessionHandoff && recentSessionEvents.length === 0)
|
|
2895
|
+
// Scope detection (CLI-side: uses cwd). api.getContext takes the resolved
|
|
2896
|
+
// scope via opts.scope to stay host-agnostic.
|
|
2897
|
+
const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
2898
|
+
const ctxActiveScope = ctxExplicitScope || detectScope();
|
|
2899
|
+
const ctx = {
|
|
2900
|
+
hippoRoot,
|
|
2901
|
+
tenantId: resolveTenantId({}),
|
|
2902
|
+
actor: 'cli',
|
|
2903
|
+
};
|
|
2904
|
+
const opts = {
|
|
2905
|
+
q: query,
|
|
2906
|
+
budget,
|
|
2907
|
+
limit: parseLimitFlag(flags['limit']),
|
|
2908
|
+
pinnedOnly,
|
|
2909
|
+
scope: ctxActiveScope ?? undefined,
|
|
2910
|
+
includeRecent: parseCountFlag(flags['include-recent']),
|
|
2911
|
+
};
|
|
2912
|
+
const result = await api.getContext(ctx, opts);
|
|
2913
|
+
// Early exit when there's nothing to render (matches pre-extraction behavior).
|
|
2914
|
+
const hasContextData = result.entries.length > 0 ||
|
|
2915
|
+
result.activeSnapshot ||
|
|
2916
|
+
result.sessionHandoff ||
|
|
2917
|
+
(result.recentEvents && result.recentEvents.length > 0);
|
|
2918
|
+
if (!hasContextData)
|
|
3110
2919
|
return;
|
|
3111
|
-
//
|
|
3112
|
-
// as read-only so pinned memories don't inflate retrieval_count or extend
|
|
3113
|
-
// their half_life by 2 days * turn-count over a long session.
|
|
3114
|
-
let updatedEntries;
|
|
3115
|
-
if (pinnedOnly) {
|
|
3116
|
-
updatedEntries = selectedItems.map((s) => s.entry);
|
|
3117
|
-
}
|
|
3118
|
-
else {
|
|
3119
|
-
// Mark retrieved and persist
|
|
3120
|
-
const toUpdate = selectedItems.map((s) => s.entry);
|
|
3121
|
-
updatedEntries = markRetrieved(toUpdate);
|
|
3122
|
-
const localIndex = loadIndex(hippoRoot);
|
|
3123
|
-
for (const u of updatedEntries) {
|
|
3124
|
-
const targetRoot = localIndex.entries[u.id] ? hippoRoot : (hasGlobal ? globalRoot : hippoRoot);
|
|
3125
|
-
writeEntry(targetRoot, u);
|
|
3126
|
-
}
|
|
3127
|
-
localIndex.last_retrieval_ids = updatedEntries.map((u) => u.id);
|
|
3128
|
-
saveIndex(hippoRoot, localIndex);
|
|
3129
|
-
updateStats(hippoRoot, { recalled: selectedItems.length });
|
|
3130
|
-
}
|
|
2920
|
+
// Format + framing are CLI rendering concerns; api.getContext doesn't see them.
|
|
3131
2921
|
const format = String(flags['format'] ?? 'markdown');
|
|
3132
2922
|
const framing = String(flags['framing'] ?? 'observe');
|
|
2923
|
+
// Adapter: ContextResultEntry -> the print-helper input shape.
|
|
2924
|
+
const renderItems = result.entries.map((r) => ({
|
|
2925
|
+
entry: r.entry,
|
|
2926
|
+
score: r.score,
|
|
2927
|
+
tokens: r.tokens,
|
|
2928
|
+
isGlobal: r.isGlobal ?? false,
|
|
2929
|
+
}));
|
|
3133
2930
|
if (format === 'json') {
|
|
3134
|
-
const output =
|
|
2931
|
+
const output = result.entries.map((r) => ({
|
|
3135
2932
|
id: r.entry.id,
|
|
3136
2933
|
score: r.score,
|
|
3137
2934
|
strength: r.entry.strength,
|
|
@@ -3140,28 +2937,31 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
3140
2937
|
content: r.entry.content,
|
|
3141
2938
|
global: r.isGlobal ?? false,
|
|
3142
2939
|
}));
|
|
3143
|
-
console.log(JSON.stringify({
|
|
2940
|
+
console.log(JSON.stringify({
|
|
2941
|
+
query: query || '*',
|
|
2942
|
+
activeSnapshot: result.activeSnapshot ?? null,
|
|
2943
|
+
sessionHandoff: result.sessionHandoff ?? null,
|
|
2944
|
+
recentSessionEvents: result.recentEvents ?? [],
|
|
2945
|
+
memories: output,
|
|
2946
|
+
tokens: result.tokens,
|
|
2947
|
+
}));
|
|
3144
2948
|
}
|
|
3145
2949
|
else if (format === 'additional-context') {
|
|
3146
|
-
// Claude Code UserPromptSubmit hook JSON shape. Capture
|
|
3147
|
-
//
|
|
2950
|
+
// Claude Code UserPromptSubmit hook JSON shape. Capture print* helpers'
|
|
2951
|
+
// output into a string buffer and wrap as `additionalContext`.
|
|
3148
2952
|
const lines = [];
|
|
3149
2953
|
const realLog = console.log;
|
|
3150
2954
|
console.log = (...parts) => { lines.push(parts.map(String).join(' ')); };
|
|
3151
2955
|
try {
|
|
3152
|
-
if (activeSnapshot)
|
|
3153
|
-
printActiveTaskSnapshot(activeSnapshot);
|
|
3154
|
-
if (sessionHandoff)
|
|
3155
|
-
printHandoff(sessionHandoff);
|
|
3156
|
-
if (
|
|
3157
|
-
printSessionEvents(
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
score: r.score,
|
|
3162
|
-
tokens: r.tokens,
|
|
3163
|
-
isGlobal: r.isGlobal ?? false,
|
|
3164
|
-
})), totalTokens, framing);
|
|
2956
|
+
if (result.activeSnapshot)
|
|
2957
|
+
printActiveTaskSnapshot(result.activeSnapshot);
|
|
2958
|
+
if (result.sessionHandoff)
|
|
2959
|
+
printHandoff(result.sessionHandoff);
|
|
2960
|
+
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
2961
|
+
printSessionEvents(result.recentEvents);
|
|
2962
|
+
}
|
|
2963
|
+
if (renderItems.length > 0) {
|
|
2964
|
+
printContextMarkdown(renderItems, result.tokens, framing);
|
|
3165
2965
|
}
|
|
3166
2966
|
}
|
|
3167
2967
|
finally {
|
|
@@ -3179,27 +2979,33 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
3179
2979
|
process.stdout.write(JSON.stringify(payload));
|
|
3180
2980
|
}
|
|
3181
2981
|
else {
|
|
3182
|
-
|
|
3183
|
-
|
|
2982
|
+
// markdown (default)
|
|
2983
|
+
if (result.activeSnapshot) {
|
|
2984
|
+
printActiveTaskSnapshot(result.activeSnapshot);
|
|
3184
2985
|
}
|
|
3185
|
-
if (sessionHandoff) {
|
|
3186
|
-
printHandoff(sessionHandoff);
|
|
2986
|
+
if (result.sessionHandoff) {
|
|
2987
|
+
printHandoff(result.sessionHandoff);
|
|
3187
2988
|
}
|
|
3188
|
-
if (
|
|
3189
|
-
printSessionEvents(
|
|
2989
|
+
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
2990
|
+
printSessionEvents(result.recentEvents);
|
|
3190
2991
|
}
|
|
3191
|
-
if (
|
|
3192
|
-
printContextMarkdown(
|
|
3193
|
-
entry: updatedEntries.find((u) => u.id === r.entry.id) ?? r.entry,
|
|
3194
|
-
score: r.score,
|
|
3195
|
-
tokens: r.tokens,
|
|
3196
|
-
isGlobal: r.isGlobal ?? false,
|
|
3197
|
-
})), totalTokens, framing);
|
|
2992
|
+
if (renderItems.length > 0) {
|
|
2993
|
+
printContextMarkdown(renderItems, result.tokens, framing);
|
|
3198
2994
|
}
|
|
3199
|
-
// Ambient state summary (
|
|
2995
|
+
// Ambient state summary (CLI-side: api.getContext doesn't load all entries
|
|
2996
|
+
// post-selection, so we re-load here for the landscape summary).
|
|
3200
2997
|
const ambientConfig = loadConfig(hippoRoot);
|
|
3201
2998
|
if (ambientConfig.ambient.enabled && !pinnedOnly) {
|
|
3202
|
-
const
|
|
2999
|
+
const tenantId = ctx.tenantId;
|
|
3000
|
+
const globalRoot = getGlobalRoot();
|
|
3001
|
+
const hasGlobalForAmbient = isInitialized(globalRoot);
|
|
3002
|
+
const localForAmbient = isInitialized(hippoRoot)
|
|
3003
|
+
? loadAllEntries(hippoRoot, tenantId).filter(e => !e.superseded_by)
|
|
3004
|
+
: [];
|
|
3005
|
+
const globalForAmbient = hasGlobalForAmbient
|
|
3006
|
+
? loadAllEntries(globalRoot, tenantId).filter(e => !e.superseded_by)
|
|
3007
|
+
: [];
|
|
3008
|
+
const allForAmbient = [...localForAmbient, ...globalForAmbient];
|
|
3203
3009
|
if (allForAmbient.length > 0) {
|
|
3204
3010
|
const ambientState = computeAmbientState(allForAmbient);
|
|
3205
3011
|
console.log(`\n${renderAmbientSummary(ambientState)}`);
|
|
@@ -3780,9 +3586,9 @@ function cmdHook(args, flags) {
|
|
|
3780
3586
|
console.log(`${hook.file} not found in ${process.cwd()} — skipping agent-instructions patch.`);
|
|
3781
3587
|
console.log(` Create ${hook.file} and re-run \`hippo hook install ${target}\` if you want the agent prompt.`);
|
|
3782
3588
|
}
|
|
3783
|
-
// For
|
|
3784
|
-
//
|
|
3785
|
-
if (target === 'claude-code'
|
|
3589
|
+
// For Claude Code, also install SessionEnd+SessionStart entries in its
|
|
3590
|
+
// settings file.
|
|
3591
|
+
if (target === 'claude-code') {
|
|
3786
3592
|
const result = installJsonHooks(target);
|
|
3787
3593
|
if (result.installedSessionEnd) {
|
|
3788
3594
|
console.log(`Installed hippo session-end SessionEnd hook in ${result.target} settings`);
|
|
@@ -3803,6 +3609,22 @@ function cmdHook(args, flags) {
|
|
|
3803
3609
|
console.log(`Migrated legacy SessionEnd entry to the new detached form`);
|
|
3804
3610
|
}
|
|
3805
3611
|
}
|
|
3612
|
+
else if (target === 'opencode') {
|
|
3613
|
+
// opencode uses a TS plugin, not JSON hooks. See src/hooks.ts.
|
|
3614
|
+
const result = installOpencodePlugin();
|
|
3615
|
+
if (result.installed) {
|
|
3616
|
+
console.log(`Installed hippo opencode plugin at ${result.pluginPath}`);
|
|
3617
|
+
}
|
|
3618
|
+
else {
|
|
3619
|
+
console.log(`opencode plugin already up to date at ${result.pluginPath}`);
|
|
3620
|
+
}
|
|
3621
|
+
if (result.migratedLegacyHooks) {
|
|
3622
|
+
console.log(`Removed legacy Claude Code-style hooks block from opencode.json — opencode can now launch`);
|
|
3623
|
+
}
|
|
3624
|
+
if (result.jsonRepairFailed) {
|
|
3625
|
+
console.log(`WARNING: opencode.json is unparseable; legacy hooks block could not be auto-removed. Fix the file manually.`);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3806
3628
|
else if (target === 'codex') {
|
|
3807
3629
|
const result = installCodexWrapper();
|
|
3808
3630
|
console.log(`Installed Codex session-end integration -> ${result.metadataPath}`);
|
|
@@ -3832,12 +3654,20 @@ function cmdHook(args, flags) {
|
|
|
3832
3654
|
else {
|
|
3833
3655
|
console.log(`${hook.file} not found, skipping agent-instructions uninstall.`);
|
|
3834
3656
|
}
|
|
3835
|
-
// For
|
|
3836
|
-
if (target === 'claude-code'
|
|
3657
|
+
// For Claude Code, also strip its SessionEnd/SessionStart entries.
|
|
3658
|
+
if (target === 'claude-code') {
|
|
3837
3659
|
if (uninstallJsonHooks(target)) {
|
|
3838
3660
|
console.log(`Removed hippo hooks from ${target} settings`);
|
|
3839
3661
|
}
|
|
3840
3662
|
}
|
|
3663
|
+
else if (target === 'opencode') {
|
|
3664
|
+
// opencode uses a TS plugin; uninstall removes the plugin file AND
|
|
3665
|
+
// also runs the legacy-hooks migration so the downgrade/remove path
|
|
3666
|
+
// leaves opencode launchable.
|
|
3667
|
+
if (uninstallOpencodePlugin()) {
|
|
3668
|
+
console.log(`Removed hippo opencode plugin (and any legacy hooks block from opencode.json)`);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3841
3671
|
else if (target === 'codex') {
|
|
3842
3672
|
if (uninstallCodexWrapper()) {
|
|
3843
3673
|
console.log('Removed Codex wrapper integration');
|
|
@@ -3866,7 +3696,7 @@ function cmdSetup(flags) {
|
|
|
3866
3696
|
const markdownTools = tools.filter((t) => t.kind === 'markdown-instruction' && t.detected);
|
|
3867
3697
|
const pluginTools = tools.filter((t) => t.kind === 'plugin' && t.detected);
|
|
3868
3698
|
if (jsonTools.length === 0 && !forceAll) {
|
|
3869
|
-
console.log('No JSON-hook-capable tools detected (checked: claude-code
|
|
3699
|
+
console.log('No JSON-hook-capable tools detected (checked: claude-code).');
|
|
3870
3700
|
console.log('Run with --all to install hooks anyway.');
|
|
3871
3701
|
}
|
|
3872
3702
|
for (const tool of jsonTools) {
|
|
@@ -3923,7 +3753,31 @@ function cmdSetup(flags) {
|
|
|
3923
3753
|
console.log('');
|
|
3924
3754
|
console.log('Plugin-based tools (hook API via plugin, not JSON):');
|
|
3925
3755
|
for (const tool of pluginTools) {
|
|
3926
|
-
|
|
3756
|
+
if (tool.name === 'opencode') {
|
|
3757
|
+
if (dryRun) {
|
|
3758
|
+
console.log(` ${tool.name.padEnd(14)} [dry-run] would install hippo plugin at ${resolveOpencodePluginPath()}`);
|
|
3759
|
+
continue;
|
|
3760
|
+
}
|
|
3761
|
+
const result = installOpencodePlugin();
|
|
3762
|
+
const bits = [];
|
|
3763
|
+
if (result.installed)
|
|
3764
|
+
bits.push('installed plugin');
|
|
3765
|
+
if (result.migratedLegacyHooks)
|
|
3766
|
+
bits.push('migrated legacy hooks block');
|
|
3767
|
+
if (result.jsonRepairFailed)
|
|
3768
|
+
bits.push('WARNING: opencode.json unparseable — manual fix needed');
|
|
3769
|
+
if (bits.length === 0) {
|
|
3770
|
+
console.log(` ${tool.name.padEnd(14)} already configured (${result.pluginPath})`);
|
|
3771
|
+
}
|
|
3772
|
+
else {
|
|
3773
|
+
console.log(` ${tool.name.padEnd(14)} ${bits.join(', ')} -> ${result.pluginPath}`);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
else {
|
|
3777
|
+
// Other plugin tools (openclaw) have their own installer; the notes
|
|
3778
|
+
// line points the user at it.
|
|
3779
|
+
console.log(` ${tool.name.padEnd(14)} ${tool.notes}`);
|
|
3780
|
+
}
|
|
3927
3781
|
}
|
|
3928
3782
|
}
|
|
3929
3783
|
if (markdownTools.length > 0) {
|