crbro-memory 2.9.1 → 2.11.0
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/LICENSE +21 -21
- package/README.md +256 -456
- package/SECURITY.md +135 -8
- package/bin/crbro.mjs +555 -299
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +143 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +762 -311
- package/dist/engine/doctor.js +676 -0
- package/dist/engine/evidence.js +171 -0
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +50 -36
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +735 -144
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +126 -69
- package/dist/engine/projects.js +80 -0
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +34 -11
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +810 -176
- package/dist/search/quality.js +31 -0
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +649 -343
- package/dist/sync/git.js +203 -72
- package/dist/sync/materialize.js +33 -8
- package/dist/sync/space.js +286 -101
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +219 -52
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +12 -8
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -670
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -61
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -156
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -323
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -35
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -142
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -116
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -346
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -11
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"build":"2.11.0-facd12b9308f70d1","latest":"2025-11-25","supported":["2025-11-25","2025-06-18","2025-03-26","2024-11-05","2024-10-07"],"initialize":{"capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"crbro-memory","version":"2.11.0"},"instructions":"CRBRO is this user's persistent memory, kept on their own machine. Start every conversation with crbro_boot: it loads what earlier sessions left — protocols to follow, open items, hot topics. Before answering OR ACTING ON anything about the user, their projects, preferences, decisions or past work, call crbro_recall: the answer is usually stored, and making them repeat it is the failure this memory exists to prevent. Recall even when you think you know. Only when the current message itself states the answer does it outrank memory: then use it — a recall that finds nothing does not make it unknown, and a stored value older than what the user just said is the one to update, not to repeat. A row in a recall's possibly_stale is a last-known value that may have changed: before answering with it, check it where it lives (the file it names, the project's files or config, or the user) and answer from that; if you cannot check, say it may be out of date — never state it as current. Then crbro_revise status=verified or crbro_learn with supersedes. Acting includes touching one of their systems: before the first command that explores or changes a project of theirs, recall what is already known about it — a stored pattern or map usually holds the very procedure you were about to reconstruct by reading files, and reconstructing it is how you end up doing the steps in the wrong order. Questions about CRBRO itself (version, counts, whether semantic recall is on) are crbro_inspect view=status. Read one entry, not a whole neuron: view=neuron gives an index, entries=[ids] the text. Save with crbro_learn as you go, and close with crbro_consolidate before the conversation ends."},"tools":{"tools":[{"name":"crbro_boot","title":"Boot the brain","description":"Read the brain at session start — call it FIRST in every conversation; writes only the boot stamp. Returns protocol_enforcement (mandatory directives: follow them), counts, open_items (due first, then newest; review:true = over a month old, no due date), recently_closed (done: never report as pending), hot_topics, recent_sessions headlines and memory_discipline; `more` names the call for what is left out. Syncs team spaces (offline is normal). Pass project (working folder or repo) to scope open items and rank its neurons first; project_neurons binds them. Close with crbro_consolidate.","inputSchema":{"type":"object","properties":{"project":{"description":"Working folder or repository; full paths tell same-named folders apart. The scope lasts for this connection.","type":"string"},"project_id":{"description":"Stable local id when declaring aliases.","type":"string"},"project_aliases":{"description":"Other folders or remotes of the same project. Nothing remote is contacted.","maxItems":31,"type":"array","items":{"type":"string"}},"project_neurons":{"description":"Neuron ids to bind to the project; replaces the binding, omitted keeps it. Unknown ids are skipped and listed in unknown_project_neurons.","maxItems":100,"type":"array","items":{"type":"string"}}}},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_inspect","title":"Inspect the brain","description":"Read-only views of the brain by id or name; nothing is written. Search by content is crbro_recall. view=status: CRBRO itself — version, brain path, totals, last boot, semantic recall, shelf-life windows, retired 1.x tool names. view=neuron: an index of one neuron (header, counts, connections, one line per entry: id, kind, date, preview), paged; entries=[ids] reads those whole; detail=full dumps it, shortened when large. Absent temporal, verification and freshness mean current_claim, none, unverified. view=neurons: rows hottest first. view=sessions: day logs newest first, where session summaries are read. view=global_map: domain clusters and bridges.","inputSchema":{"type":"object","properties":{"view":{"type":"string","enum":["status","neuron","neurons","sessions","global_map"],"description":"Which read. Parameters for other views are ignored, never an error."},"neuron":{"description":"view=neuron, required: id (\"project_octochat\") or name.","type":"string"},"detail":{"description":"view=neuron: index (default, cheap) or full.","type":"string","enum":["index","full"]},"entries":{"description":"view=neuron: entry ids or exact text, any kind (\"map\" for the map); ignores detail.","type":"array","items":{"type":"string"}},"session":{"description":"view=sessions: one day log whole, e.g. \"session_2026-09-07\".","type":"string"},"domain":{"description":"view=neurons: exact domain.","type":"string"},"type":{"description":"view=neurons: only this type.","type":"string","enum":["project","tech","lang","person","domain","process","protocol"]},"min_heat":{"description":"view=neurons: minimum heat.","type":"number","minimum":0,"maximum":1},"min_strength":{"description":"view=neuron: hide weaker connections.","type":"number","minimum":0,"maximum":1},"limit":{"description":"Page size (neuron 25/200 max, neurons 50/500, sessions 10/100).","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"offset":{"description":"Items to skip (neuron index, neurons).","type":"integer","minimum":0,"maximum":9007199254740991},"include_superseded":{"description":"view=neuron: list retired entries too (default false).","type":"boolean"}},"required":["view"]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"outputSchema":{"type":"object","properties":{"view":{"type":"string","enum":["status","neuron","neurons","sessions","global_map"]},"status":{"type":"object","properties":{"crbro_version":{"type":"string"}},"required":["crbro_version"],"additionalProperties":{}},"neuron":{"type":"object","properties":{},"additionalProperties":{}},"neurons":{"type":"object","properties":{"total":{"type":"number"}},"required":["total"],"additionalProperties":{}},"sessions":{"type":"object","properties":{"total":{"type":"number"}},"required":["total"],"additionalProperties":{}},"global_map":{"type":"object","properties":{"total_clusters":{"type":"number"}},"required":["total_clusters"],"additionalProperties":{}}},"required":["view"],"additionalProperties":false}},{"name":"crbro_learn","title":"Learn something","description":"Write a fact, decision, pattern, preference, error or debt to a neuron (created when the topic matches none). Recall first: it may exist. A replacement passes supersedes; retiring without one is crbro_revise; deleting is crbro_forget. A value that can change names its source (file, key, URL, person). Repeating a fact merges metadata, never verifies it (crbro_revise status=verified does). One claim per fact: past ~600 characters the answer suggests splitting; a huge neuron, crbro_revise move_to. Credentials become markers: crbro_secret them, store only the name. Returns neuron_id, action, shelf_life, near_duplicates, supersedes_unmatched (still live).","inputSchema":{"type":"object","properties":{"topic":{"description":"Topic name, e.g. \"OctoChat\"; or pass neuron_id.","type":"string"},"type":{"type":"string","enum":["fact","decision","pattern","preference","error","debt"],"description":"error = a mistake with its fix; debt = a deliberate deferral with its ceiling and revisit trigger."},"content":{"type":"string","description":"Self-contained: recalled without this conversation. For a changeable value, say where it came from (file, key, URL, person)."},"confidence":{"description":"Facts: 0-1, default 1.","type":"number","minimum":0,"maximum":1},"domain":{"description":"Set on a new neuron, or replacing \"general\".","type":"string"},"rationale":{"description":"Decisions: why. Indexed with it.","type":"string"},"neuron_id":{"description":"Exact id from crbro_recall; skips name matching.","type":"string"},"supersedes":{"description":"Ids or exact text of the facts this replaces; unmatched ones stay live.","type":"array","items":{"type":"string"}},"keywords":{"description":"Facts, expected: 2-5 words a future question may use that the text lacks (synonyms, other language, generic name).","type":"array","items":{"type":"string"}},"keywords_replace":{"description":"Replace, not merge, the keywords of an identical fact.","type":"boolean"},"shelf_life":{"description":"Facts: volatile (versions, ports, hosts, config, roles) to permanent (history). Inferred if omitted.","type":"string","enum":["volatile","normal","durable","permanent"]},"reference":{"description":"Facts: where to check it (relative file, URL, person). Data, never fetched.","type":"object","properties":{"kind":{"type":"string","enum":["file","url","person"]},"value":{"type":"string","minLength":1,"maxLength":2048},"field":{"type":"string","maxLength":256},"project":{"type":"string","maxLength":256}},"required":["kind","value"]},"temporal":{"description":"Facts: current state (default) or a historical record.","type":"string","enum":["current","historical"]}},"required":["type","content"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_recall","title":"Recall","description":"Read-only search of everything saved in earlier sessions. Call it BEFORE answering anything about the user, their projects, preferences, decisions or past work: the answer is usually stored. One row per neuron: the best entry (matching_content; entry_id → crbro_inspect view=neuron entries=[id] reads it whole), confidence (weak: verify; a search match, not truth) and also_matched previews. Absent fields hold their default: temporal current_claim, verification none (present = a check the caller reported), freshness unverified, provenance local. Lines not the user's own carry origin, also_matched too: team:<space> with self-declared by, or miner. Rows past their shelf life move to possibly_stale as last_known with a next_step: check before answering with one. has_map: read crbro_map before touching that system.","inputSchema":{"type":"object","properties":{"query":{"type":"string","description":"What to look for; a few distinctive terms beat a sentence."},"queries":{"description":"Other phrasings searched with query and fused: synonyms, the other language, product names. 2-4.","type":"array","items":{"type":"string"}},"domain":{"description":"Only neurons of this exact domain.","type":"string"},"limit":{"description":"Rows, default 5.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"since":{"description":"A day (\"2026-09-01\") or span (\"7d\", \"2w\", \"3m\"); undated entries are skipped and counted.","type":"string"},"kind":{"description":"Only these entry kinds, e.g. [\"error\"] before repeating a mistake. Leaves out day logs.","type":"array","items":{"type":"string","enum":["fact","decision","pattern","preference","error","debt","map"]}},"project":{"description":"Project id or alias; default the boot project of this connection.","type":"string"},"scope":{"description":"auto (default) ranks the bound project first; project restricts to it; all ignores it.","type":"string","enum":["auto","project","all"]},"neurons":{"description":"Only these neuron ids; no day logs.","maxItems":100,"type":"array","items":{"type":"string"}},"intent":{"description":"current splits historical records into historical_matches; history keeps only them; any (default).","type":"string","enum":["any","current","history"]}},"required":["query"]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"outputSchema":{"type":"object","properties":{"stale_warning":{"type":"string"},"query":{"type":"string"},"total_results":{"type":"number"},"returned":{"type":"number"},"has_more":{"type":"boolean"},"results":{"type":"array","items":{"type":"object","properties":{"neuron_id":{"type":"string"},"name":{"type":"string"},"matching_content":{"type":"string"},"confidence":{"type":"string","enum":["strong","weak"]},"entry_id":{"type":"string"},"origin":{"type":"string"},"also_matched":{"type":"array","items":{"type":"object","properties":{"preview":{"type":"string"},"origin":{"type":"string"},"stale_days":{"type":"number"}},"required":["preview"],"additionalProperties":{}}}},"required":["neuron_id","name","matching_content"],"additionalProperties":{}}},"possibly_stale":{"type":"array","items":{"type":"object","properties":{"next_step":{"type":"string"},"last_known":{"type":"string"},"neuron_id":{"type":"string"}},"required":["next_step","last_known","neuron_id"],"additionalProperties":{}}},"possibly_stale_count":{"type":"number"},"historical_matches":{"type":"array","items":{}},"sessions_matched":{"type":"array","items":{}},"hint":{"type":"string"},"no_answer":{"description":"Nothing cleared the evidence bar: at most 3 weak rows, the nearest","type":"boolean"},"semantic":{"description":"warming: model still loading, keyword-only answer","type":"string"}},"required":["query","total_results","results","hint"],"additionalProperties":{}}},{"name":"crbro_revise","title":"Revise a neuron","description":"Write: change what a neuron says without deleting anything — for what stopped being true, or never was, with nothing replacing it (a replacement is crbro_learn with supersedes; what must not exist on disk, crbro_forget). facts and entries take a status: superseded or retracted retire them (kept in the file, gone from recall), active reactivates, and status verified records a check against the source. summary, domain, tags and name edit metadata in the same call; the id never changes. move_to splits: the listed entries go to another neuron with their dates. Anything in unmatched is STILL LIVE: fix and re-run.","inputSchema":{"type":"object","properties":{"neuron":{"type":"string","description":"Neuron id or name."},"facts":{"description":"Fact ids (entry_id from crbro_recall) or exact text.","type":"array","items":{"type":"string"}},"entries":{"description":"Decisions, patterns, errors or debts by exact text (or id with verified and move_to).","type":"array","items":{"type":"string"}},"status":{"description":"superseded (default: newer truth exists), retracted (never true), active (reactivate) or verified (still holds at its source).","type":"string","enum":["superseded","retracted","active","verified"]},"note":{"description":"Why; stored with the retirement.","type":"string"},"evidence":{"description":"With verified: source = you checked the reference; user = the user confirmed.","type":"object","properties":{"method":{"type":"string","enum":["source","user"]},"reference":{"type":"object","properties":{"kind":{"type":"string","enum":["file","url","person"]},"value":{"type":"string","minLength":1,"maxLength":2048},"field":{"type":"string","maxLength":256},"project":{"type":"string","maxLength":256}},"required":["kind","value"]}},"required":["method"]},"summary":{"description":"New neuron summary; credentials are redacted.","type":"string"},"domain":{"description":"New domain.","type":"string"},"tags":{"description":"Replaces the WHOLE tag list; on protocol neurons re-send priority: and source:.","type":"array","items":{"type":"string"}},"name":{"description":"Rename; id, file and links stay.","type":"string"},"move_to":{"description":"Split: move the listed facts/entries to this neuron id or name (created if missing), dates kept. Refused on a shared neuron.","type":"string"}},"required":["neuron"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_forget","title":"Forget for good","description":"Write, destructive: remove from disk after a quarantine copy (backup returned) — for what must not exist on disk: a credential, personal data, a whole neuron. What merely stopped being true goes to crbro_revise, which keeps the history. One mode per call: facts (entries of any kind, or the map); entire (a neuron and its synapses, in two steps: without confirm_token it is a dry run — show the user, get agreement, call again with the token); restore (the newest quarantine copy); merge_into (union a neuron into another, delete the source); session (one day log). entire and merge_into refuse a shared neuron. Rotate any removed credential.","inputSchema":{"type":"object","properties":{"neuron":{"description":"Neuron id or name (restore: exact id). Not needed for session.","type":"string"},"facts":{"description":"Entry ids or exact text; the exact map text removes the map.","type":"array","items":{"type":"string"}},"entire":{"description":"The whole neuron; without confirm_token a dry run.","type":"boolean"},"confirm_token":{"description":"From the dry run; refused if the neuron changed since.","type":"string"},"restore":{"description":"Bring back the newest quarantine copy (merged in if the neuron exists again).","type":"boolean"},"merge_into":{"description":"Target neuron id or name; `neuron` is unioned into it, then deleted.","type":"string"},"session":{"description":"Day log id (\"session_2026-09-03\" or \"2026-09-03\").","type":"string"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_connect","title":"Connect two neurons","description":"Write: create, strengthen or delete the undirected synapse between two neurons (ids validated). action=connect (default) creates it at 0.5 and adds 0.1 per repeat (cap 1.0), or sets the strength given; action=disconnect deletes it — the destructive side. crbro_consolidate already links neurons written in one session: use this for relationships beyond co-occurrence. Read links with crbro_inspect view=neuron. Returns synapse_id, action (created, strengthened, disconnected, absent) and strength.","inputSchema":{"type":"object","properties":{"action":{"description":"connect (default) or disconnect; an absent synapse is action:absent, not an error.","type":"string","enum":["connect","disconnect"]},"from":{"type":"string","description":"Exact neuron id, e.g. \"project_octochat\"."},"to":{"type":"string","description":"Exact neuron id; order does not matter."},"type":{"description":"Set on creation only (default conceptual).","type":"string","enum":["dependency","causal","temporal","conceptual","hierarchy","alternative"]},"strength":{"description":"Absolute strength to set instead of the 0.5 / +0.1 rule.","type":"number","minimum":0,"maximum":1},"context":{"description":"One line on the relationship; replaces the stored one.","type":"string"}},"required":["from","to"]},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_context","title":"Working context","description":"Read or write the working context: active topics and open items. With no change field it reads (written:false) everything: each open item whole with due and done_when, and recently closed. A write returns what changed and the counts (return_context:true for the whole context). Scope: this connection's project plus global items; scope=all spans every project. resolve_pending closes items (the last 15 stay in recently_closed); discard_pending drops them without recording them as done. Prefer ids. Nothing is deleted by age. Sessions are logged by crbro_consolidate.","inputSchema":{"type":"object","properties":{"project":{"description":"Project id or alias; default the boot project.","type":"string"},"scope":{"description":"all spans every project.","type":"string","enum":["project","all"]},"due":{"description":"With add_pending: a calendar day YYYY-MM-DD, only when agreed.","type":"string"},"done_when":{"description":"With add_pending: an observable completion condition.","type":"string"},"set_topics":{"description":"Replace the active topics with these neuron ids.","type":"array","items":{"type":"string"}},"add_pending":{"description":"An open item, checkable later. Identical text is deduplicated; re-adding updates due and done_when.","type":"string"},"resolve_pending":{"description":"Close items by id (\"p_ab12cd\") or 8+ characters of their text; several can match. Empty resolved = no match.","type":"string"},"discard_pending":{"description":"Drop items, same matcher, without recording them as done.","type":"string"},"clear":{"description":"Empty topics, open and closed items (of the project only, when scoped), before the rest.","type":"boolean"},"return_context":{"description":"With a write: return the whole context instead of the change and counts.","type":"boolean"}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_map","title":"System map","description":"Read or replace a neuron's system map: ONE living document — where the system lives, what serves what, the traps that cost hours. Read it before working on a system touched in past sessions; after changing it, rewrite it whole. Omit content to read (map:null if none); content replaces the old map entirely; an empty string clears it. crbro_inspect view=neuron also returns it. Writing creates the neuron if needed and redacts credentials. Atomic facts go to crbro_learn — the map is the prose around them.","inputSchema":{"type":"object","properties":{"neuron":{"type":"string","description":"Neuron id or name."},"content":{"description":"The whole new map (omit to read): paths, ids, what serves what, gotchas.","type":"string"},"domain":{"description":"Domain for a neuron created by the write.","type":"string"}},"required":["neuron"]},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_consolidate","title":"Consolidate the session","description":"Write: close the session — the only way to log one; call it before the conversation ends, or the session's knowledge is lost. Logs the summary (credentials stripped), flushes the index, recalculates heat, links the neurons written this session and syncs team spaces (offline is normal). Returns session_id, facts_saved, decisions_saved, topics_touched and sync state; promotion_candidates: own lessons found in 2+ project neurons, with a suggested_target (nothing moves). Mid-session open items go to crbro_context; housekeeping, crbro_maintenance.","inputSchema":{"type":"object","properties":{"summary":{"type":"string","description":"A headline paragraph, not a report: what was done, decided and left open. Facts go to crbro_learn, where recall finds them as facts; this is narrative."},"topics_touched":{"description":"Neuron ids used without writing (recalled, inspected); unknown ids go to topics_unknown.","type":"array","items":{"type":"string"}}},"required":["summary"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_maintenance","title":"Brain maintenance","description":"Write: brain housekeeping — recalculate heat, prune weak synapses, check integrity, rebuild the search index. Returns a report (counts, integrity_issues, repairable, notes) and flags debts without a revisit trigger. dry_run:true writes nothing. Extras run only when asked: archive cold neurons (they stop being searchable), unarchive, purge_boilerplate, repair, backfill_dates, compact. Session close is crbro_consolidate; reading the brain is crbro_inspect.","inputSchema":{"type":"object","properties":{"dry_run":{"description":"Report only: nothing is written. Counts, debts and integrity checks still run.","type":"boolean"},"archive":{"description":"Move cold neurons (heat < 0.05, untouched 90+ days) to archives/, out of search. Dry-run first (archivable_ids); undo with unarchive.","type":"boolean"},"unarchive":{"description":"Neuron ids (or \"all\") to bring back from archives/ and reindex.","anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string","const":"all"}]},"purge_boilerplate":{"description":"Delete contentless facts the miner left (counted in every report); a quarantine copy is kept.","type":"boolean"},"repair":{"description":"Fix what the integrity check found (dangling links, orphan synapses, stale keys, counters); lists repairs[].","type":"boolean"},"backfill_dates":{"description":"Date pre-1.13 entries from a day stated in their own text; nothing is guessed (dates_backfilled).","type":"boolean"},"compact":{"description":"Fold one-line neurons from a bulk import (compact_groups) into one digest per group; sources copied to quarantine first, shared neurons untouched. Dry-run first.","type":"boolean"},"stale_list":{"description":"Read-only: page through every stale entry, most overdue first (default limit 50). Runs alone.","type":"object","properties":{"offset":{"type":"integer","minimum":0,"maximum":9007199254740991},"limit":{"type":"integer","minimum":1,"maximum":200},"neuron_id":{"type":"string"}}},"stale_review":{"description":"Act on many stale entries: verify (still true; its clock restarts) or retire them (superseded / retracted). Takes the pairs stale_list returns; runs alone, not with dry_run.","type":"object","properties":{"action":{"type":"string","enum":["verify","superseded","retracted"]},"entries":{"minItems":1,"maxItems":500,"type":"array","items":{"type":"object","properties":{"neuron_id":{"type":"string"},"entry_id":{"type":"string"}},"required":["neuron_id","entry_id"]}},"note":{"type":"string"}},"required":["action","entries"]}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_audit","title":"Audit for credentials","description":"Read-only scan of every neuron (all entry kinds and the map) and every session log for credentials stored before the filter caught them: API keys, tokens, passwords. Reports where and what kind, never the values. Findings are also in the search index, so recall can return them: remove them with crbro_forget (facts for entries, session for a day log), then rotate them. Run it after upgrading or when a secret may have been pasted. crbro_inspect shows content; this judges it.","inputSchema":{"type":"object","properties":{}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},"execution":{"taskSupport":"forbidden"},"outputSchema":{"type":"object","properties":{"neurons_affected":{"type":"number"},"facts_affected":{"type":"number"},"findings":{"type":"array","items":{"type":"object","properties":{"neuron_id":{"type":"string"},"kinds":{"type":"array","items":{"type":"string"}}},"required":["neuron_id","kinds"],"additionalProperties":{}}},"sessions_affected":{"type":"number"},"session_findings":{"type":"array","items":{"type":"object","properties":{"session_id":{"type":"string"},"kinds":{"type":"array","items":{"type":"string"}}},"required":["session_id","kinds"],"additionalProperties":{}}},"message":{"type":"string"},"note":{"type":"string"}},"required":["neurons_affected","facts_affected","findings","message"],"additionalProperties":false}},{"name":"crbro_secret","title":"Keychain secret","description":"Read or write credentials in the OS keychain (macOS Keychain, Linux Secret Service, Windows DPAPI); no sync or team space reaches it. When the user hands you a credential, set it, then crbro_learn only the NAME. get returns value and source: never print it unless asked; missing = found:false. Environment variables count only as CRBRO_SECRET_<NAME> or when listed in CRBRO_SECRET_ENV. Overwriting with a different value, and remove, are two-step: the first call changes nothing and returns confirm_token; show the user, get agreement, call again with confirm. A missing store file is an error, never empty; on Windows `crbro-memory secret restore` puts back a sealed copy.","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["get","set","list","remove","status"],"description":"get one, set (store or update) one, list names only, remove one, status = which keychain this machine offers."},"name":{"description":"SCREAMING_SNAKE_CASE, e.g. WORDPRESS_APP_PASSWORD. For get, set, remove.","type":"string"},"value":{"description":"The credential, non-empty. Only for set.","type":"string"},"description":{"description":"What it is for. Only for set.","type":"string"},"confirm":{"description":"confirm_token from the first call of an overwrite or remove.","type":"string"}},"required":["action"]},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":false},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_space","title":"Team space","description":"Read or write team spaces: shared memory in a private git repository holding only the projects you choose to share. create starts one; join clones a teammate's; status shows your identity and spaces; sync exchanges notes now (boot and consolidate do it on their own; offline is a normal answer); leave pushes pending notes and deletes the local copy, keeping your neurons. Joining shares nothing: each project goes in with crbro_share. create and join reply ok:false with the reason on failure.","inputSchema":{"type":"object","properties":{"action":{"type":"string","enum":["create","join","status","sync","leave"],"description":"leave = sync, then forget the space locally."},"name":{"description":"Short name, the same on every machine, e.g. \"equipo\". Required for create, join, leave; sync without it syncs all.","type":"string"},"remote":{"description":"Git URL of an EMPTY private repository for create; the creator's URL for join.","type":"string"},"author":{"description":"How your notes are signed, lowercase, e.g. \"ana\". Required for create and join.","type":"string"},"branch":{"description":"create and join: branch (default \"main\").","type":"string"}},"required":["action"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":true},"execution":{"taskSupport":"forbidden"}},{"name":"crbro_share","title":"Share a project","description":"Write: put one neuron into a team space, or take it out with unshare. Without confirm it is a dry run: it reports what would be sent (ops_to_emit; preferences never leave this machine) and refuses outright if it finds a credential (crbro_forget it, rotate it). Show the user, get agreement, call again with the confirm token; a stale token is refused. From then on what is learned about that project reaches the team on each crbro_space sync or consolidate. unshare stops future notes; what was sent stays with the team.","inputSchema":{"type":"object","properties":{"neuron":{"type":"string","description":"Neuron id or name."},"space":{"description":"Space name from crbro_space; required unless unshare.","type":"string"},"confirm":{"description":"The dry run's confirm_token (given only when no credential was found).","type":"string"},"unshare":{"description":"Stop sending notes for `neuron`; what was sent stays with the team. space and confirm are ignored.","type":"boolean"}},"required":["neuron"]},"annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":true},"execution":{"taskSupport":"forbidden"}}]}}
|
package/dist/miner/extractor.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// No AI required — uses regex patterns and basic NLP.
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.extractContent = extractContent;
|
|
7
|
+
exports.isMinerNoise = isMinerNoise;
|
|
7
8
|
// Common technology keywords to detect
|
|
8
9
|
const TECH_KEYWORDS = new Set([
|
|
9
10
|
'react', 'vue', 'angular', 'svelte', 'next.js', 'nextjs', 'nuxt', 'vite',
|
|
@@ -87,7 +88,7 @@ function extractContent(text, filename) {
|
|
|
87
88
|
let match;
|
|
88
89
|
while ((match = pattern.exec(text)) !== null) {
|
|
89
90
|
const decision = match[0].trim();
|
|
90
|
-
if (decision.length > 10 && decision.length < 200) {
|
|
91
|
+
if (decision.length > 10 && decision.length < 200 && !isMinerNoise(cleanExtract(decision))) {
|
|
91
92
|
decisions.push(cleanExtract(decision));
|
|
92
93
|
}
|
|
93
94
|
}
|
|
@@ -98,7 +99,7 @@ function extractContent(text, filename) {
|
|
|
98
99
|
let match;
|
|
99
100
|
while ((match = pattern.exec(text)) !== null) {
|
|
100
101
|
const fact = match[0].trim();
|
|
101
|
-
if (fact.length > 10 && fact.length < 200) {
|
|
102
|
+
if (fact.length > 10 && fact.length < 200 && !isMinerNoise(cleanExtract(fact))) {
|
|
102
103
|
facts.push(cleanExtract(fact));
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -123,29 +124,75 @@ function cleanExtract(text) {
|
|
|
123
124
|
.trim();
|
|
124
125
|
}
|
|
125
126
|
/**
|
|
126
|
-
*
|
|
127
|
+
* Lines that are scaffolding, not knowledge: a checklist item ("- [x] Navigate
|
|
128
|
+
* to https://…", or "- x Navigate to …" once the brackets were stripped, which
|
|
129
|
+
* is how ~300 of them reached a real brain as facts), a bare link, a heading,
|
|
130
|
+
* a rule. Exported for crbro_maintenance, which finds the ones already stored
|
|
131
|
+
* (purge_boilerplate, reviewed with dry_run first).
|
|
132
|
+
*/
|
|
133
|
+
function isMinerNoise(text) {
|
|
134
|
+
const t = (text || '').trim();
|
|
135
|
+
if (!t)
|
|
136
|
+
return true;
|
|
137
|
+
if (/^#{1,6}\s/.test(t))
|
|
138
|
+
return true; // heading
|
|
139
|
+
if (/^[-*+]\s*\[[ xX~-]?\]/.test(t))
|
|
140
|
+
return true; // - [ ] / - [x] checklist
|
|
141
|
+
if (/^[-*+]\s+[xX]\s+\S/.test(t))
|
|
142
|
+
return true; // "- x Navigate…": a checklist after cleanup
|
|
143
|
+
if (/^(?:[-*+]\s*)?<?(?:https?|file):\/\/\S+>?\.?$/i.test(t))
|
|
144
|
+
return true; // a bare link
|
|
145
|
+
if (/^[-*_=~]{3,}$/.test(t))
|
|
146
|
+
return true; // a rule
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
/** A sentence somebody wrote, not a list item, a table row, a quote, markup or a link with a few words around it. */
|
|
150
|
+
function isProse(line) {
|
|
151
|
+
const t = line.trim();
|
|
152
|
+
if (isMinerNoise(t))
|
|
153
|
+
return false;
|
|
154
|
+
if (/^(?:[-*+]|\d+[.)])\s/.test(t))
|
|
155
|
+
return false; // list item
|
|
156
|
+
if (/^[|>]/.test(t) || /^<\/?[a-z]/i.test(t))
|
|
157
|
+
return false; // table, quote, HTML
|
|
158
|
+
const words = t.split(/\s+/).filter(w => /[\p{L}]/u.test(w));
|
|
159
|
+
if (words.length < 5)
|
|
160
|
+
return false;
|
|
161
|
+
const urls = (t.match(/\S*:\/\/\S+/g) || []).join('').length;
|
|
162
|
+
return urls <= t.length * 0.4;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A one-line summary from the first real sentence: never a heading, a list or
|
|
166
|
+
* checklist item, a table row, a link, code or frontmatter. Nothing when the
|
|
167
|
+
* file has no prose — an empty summary is better than a checklist line stored
|
|
168
|
+
* as a fact.
|
|
127
169
|
*/
|
|
128
170
|
function generateSummary(lines) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
171
|
+
let inCode = false;
|
|
172
|
+
let inFrontmatter = false;
|
|
173
|
+
for (let i = 0; i < lines.length; i++) {
|
|
174
|
+
const trimmed = lines[i].trim();
|
|
175
|
+
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
176
|
+
inCode = !inCode;
|
|
133
177
|
continue;
|
|
134
|
-
|
|
178
|
+
}
|
|
179
|
+
if (inCode)
|
|
135
180
|
continue;
|
|
136
|
-
if (trimmed
|
|
181
|
+
if (i === 0 && trimmed === '---') {
|
|
182
|
+
inFrontmatter = true;
|
|
137
183
|
continue;
|
|
138
|
-
|
|
184
|
+
}
|
|
185
|
+
if (inFrontmatter) {
|
|
186
|
+
if (trimmed === '---')
|
|
187
|
+
inFrontmatter = false;
|
|
139
188
|
continue;
|
|
140
|
-
|
|
189
|
+
}
|
|
190
|
+
if (trimmed.length <= 30 || !isProse(trimmed))
|
|
141
191
|
continue;
|
|
142
|
-
|
|
192
|
+
const clean = cleanExtract(trimmed);
|
|
193
|
+
if (isMinerNoise(clean))
|
|
143
194
|
continue;
|
|
144
|
-
|
|
145
|
-
if (trimmed.length > 30) {
|
|
146
|
-
const clean = cleanExtract(trimmed);
|
|
147
|
-
return clean.length > 150 ? clean.substring(0, 150) + '...' : clean;
|
|
148
|
-
}
|
|
195
|
+
return clean.length > 150 ? clean.substring(0, 150) + '...' : clean;
|
|
149
196
|
}
|
|
150
197
|
return '';
|
|
151
198
|
}
|
package/dist/miner/scheduler.js
CHANGED
|
@@ -2,28 +2,90 @@
|
|
|
2
2
|
// ─── CRBRO Miner: Cross-Platform Scheduler ───────────────────
|
|
3
3
|
// Sets up automatic mining as a scheduled task on any OS.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports._setSchedulerRunnerForTests = _setSchedulerRunnerForTests;
|
|
6
|
+
exports.cronWithout = cronWithout;
|
|
7
|
+
exports.windowsTaskCommand = windowsTaskCommand;
|
|
8
|
+
exports.windowsCreateArgs = windowsCreateArgs;
|
|
5
9
|
exports.setupScheduler = setupScheduler;
|
|
6
10
|
exports.getSchedulerStatus = getSchedulerStatus;
|
|
7
11
|
exports.removeScheduler = removeScheduler;
|
|
12
|
+
exports.linuxCronLine = linuxCronLine;
|
|
13
|
+
exports.findNpxPath = findNpxPath;
|
|
8
14
|
const promises_1 = require("fs/promises");
|
|
9
15
|
const path_1 = require("path");
|
|
10
16
|
const os_1 = require("os");
|
|
11
17
|
const child_process_1 = require("child_process");
|
|
12
18
|
const fs_1 = require("fs");
|
|
19
|
+
const realRunner = (cmd, args, input) => {
|
|
20
|
+
const r = (0, child_process_1.spawnSync)(cmd, args, { encoding: 'utf-8', input, windowsHide: true, timeout: 30_000 });
|
|
21
|
+
return { status: r.status, stdout: r.stdout || '', stderr: r.stderr || '', error: r.error };
|
|
22
|
+
};
|
|
23
|
+
let run = realRunner;
|
|
24
|
+
let platformOverride = null;
|
|
25
|
+
const currentPlatform = () => platformOverride ?? (0, os_1.platform)();
|
|
26
|
+
/** Tests only: replace the command runner and, optionally, the platform. */
|
|
27
|
+
function _setSchedulerRunnerForTests(r, os = null) {
|
|
28
|
+
run = r ?? realRunner;
|
|
29
|
+
platformOverride = r ? os : null;
|
|
30
|
+
}
|
|
31
|
+
function must(r, what) {
|
|
32
|
+
if (r.error || r.status !== 0) {
|
|
33
|
+
throw new Error(`${what} failed: ${(r.stderr || r.error?.message || `exit ${r.status}`).trim()}`);
|
|
34
|
+
}
|
|
35
|
+
return r;
|
|
36
|
+
}
|
|
37
|
+
/** Where the miner logs and where a copy of the previous crontab goes. */
|
|
38
|
+
function brainRoot() {
|
|
39
|
+
return process.env['CRBRO_PATH'] || (0, path_1.join)((0, os_1.homedir)(), '.crbro');
|
|
40
|
+
}
|
|
41
|
+
/** The current crontab, or '' when the user has none. Any other failure throws. */
|
|
42
|
+
function readCrontab() {
|
|
43
|
+
const r = run('crontab', ['-l']);
|
|
44
|
+
if (r.status === 0)
|
|
45
|
+
return r.stdout;
|
|
46
|
+
if (/no crontab/i.test(r.stderr))
|
|
47
|
+
return '';
|
|
48
|
+
throw new Error(`Could not read the crontab: ${(r.stderr || r.error?.message || '').trim() || `exit ${r.status}`}`);
|
|
49
|
+
}
|
|
50
|
+
/** Keep the previous crontab next to the brain, then install the new one through stdin. */
|
|
51
|
+
async function writeCrontab(previous, next) {
|
|
52
|
+
if (previous.trim()) {
|
|
53
|
+
await (0, promises_1.mkdir)(brainRoot(), { recursive: true });
|
|
54
|
+
const stamp = new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15);
|
|
55
|
+
await (0, promises_1.writeFile)((0, path_1.join)(brainRoot(), `crontab.before-crbro-${stamp}.txt`), previous, 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
must(run('crontab', ['-'], next), 'crontab');
|
|
58
|
+
}
|
|
59
|
+
/** The cron file without our line, the user's lines byte for byte. */
|
|
60
|
+
function cronWithout(current) {
|
|
61
|
+
return current.split('\n').filter(l => !l.includes('crbro-memory mine'));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The /TR value for schtasks: the quoted absolute path of npx.cmd and its
|
|
65
|
+
* arguments, as one argument. Passed as an array element, not through a
|
|
66
|
+
* shell, so the quotes reach schtasks intact (I11).
|
|
67
|
+
*/
|
|
68
|
+
function windowsTaskCommand(npxPath) {
|
|
69
|
+
return `"${npxPath}" -y crbro-memory mine`;
|
|
70
|
+
}
|
|
71
|
+
function windowsCreateArgs(npxPath) {
|
|
72
|
+
return ['/Create', '/TN', TASK_NAME, '/TR', windowsTaskCommand(npxPath),
|
|
73
|
+
'/SC', 'HOURLY', '/MO', String(FREQUENCY_HOURS), '/F', '/RL', 'LIMITED'];
|
|
74
|
+
}
|
|
13
75
|
const TASK_NAME = 'CRBRO-AutoMiner';
|
|
14
76
|
const FREQUENCY_HOURS = 2;
|
|
15
77
|
/**
|
|
16
78
|
* Detect the platform and set up the appropriate scheduler.
|
|
17
79
|
*/
|
|
18
80
|
async function setupScheduler() {
|
|
19
|
-
const os = (
|
|
81
|
+
const os = currentPlatform();
|
|
20
82
|
switch (os) {
|
|
21
83
|
case 'win32':
|
|
22
84
|
return setupWindows();
|
|
23
85
|
case 'darwin':
|
|
24
86
|
return setupMacOS();
|
|
25
87
|
case 'linux':
|
|
26
|
-
return setupLinux();
|
|
88
|
+
return await setupLinux();
|
|
27
89
|
default:
|
|
28
90
|
return {
|
|
29
91
|
success: false,
|
|
@@ -38,7 +100,7 @@ async function setupScheduler() {
|
|
|
38
100
|
* Check if the scheduler is already installed.
|
|
39
101
|
*/
|
|
40
102
|
async function getSchedulerStatus() {
|
|
41
|
-
const os = (
|
|
103
|
+
const os = currentPlatform();
|
|
42
104
|
switch (os) {
|
|
43
105
|
case 'win32':
|
|
44
106
|
return getWindowsStatus();
|
|
@@ -60,22 +122,26 @@ async function getSchedulerStatus() {
|
|
|
60
122
|
* Remove the scheduled task.
|
|
61
123
|
*/
|
|
62
124
|
async function removeScheduler() {
|
|
63
|
-
const os = (
|
|
125
|
+
const os = currentPlatform();
|
|
64
126
|
try {
|
|
65
127
|
switch (os) {
|
|
66
128
|
case 'win32':
|
|
67
|
-
(
|
|
129
|
+
must(run('schtasks', ['/Delete', '/TN', TASK_NAME, '/F']), 'schtasks /Delete');
|
|
68
130
|
return { success: true, message: `Removed Windows Task Scheduler entry: ${TASK_NAME}` };
|
|
69
131
|
case 'darwin': {
|
|
70
132
|
const plistPath = (0, path_1.join)((0, os_1.homedir)(), 'Library', 'LaunchAgents', `com.crbro.miner.plist`);
|
|
71
|
-
(
|
|
72
|
-
(0,
|
|
133
|
+
must(run('launchctl', ['unload', plistPath]), 'launchctl unload');
|
|
134
|
+
await (0, promises_1.unlink)(plistPath);
|
|
73
135
|
return { success: true, message: `Removed macOS LaunchAgent: ${plistPath}` };
|
|
74
136
|
}
|
|
75
137
|
case 'linux': {
|
|
76
|
-
const currentCron = (
|
|
77
|
-
const filtered = currentCron
|
|
78
|
-
|
|
138
|
+
const currentCron = readCrontab();
|
|
139
|
+
const filtered = cronWithout(currentCron);
|
|
140
|
+
if (filtered.length === currentCron.split('\n').length) {
|
|
141
|
+
return { success: true, message: 'No crontab entry for the CRBRO miner. Nothing changed.' };
|
|
142
|
+
}
|
|
143
|
+
const next = filtered.join('\n').replace(/\n*$/, '\n');
|
|
144
|
+
await writeCrontab(currentCron, next.trim() ? next : '');
|
|
79
145
|
return { success: true, message: 'Removed crontab entry for CRBRO miner' };
|
|
80
146
|
}
|
|
81
147
|
default:
|
|
@@ -90,17 +156,17 @@ async function removeScheduler() {
|
|
|
90
156
|
function setupWindows() {
|
|
91
157
|
try {
|
|
92
158
|
// Remove existing task if present
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Find npx path
|
|
159
|
+
// Remove an existing task if present (a failure here only means there was none).
|
|
160
|
+
run('schtasks', ['/Delete', '/TN', TASK_NAME, '/F']);
|
|
161
|
+
// npx.cmd, absolute: Task Scheduler cannot run the extensionless `npx`
|
|
162
|
+
// shell script that `where npx` lists first.
|
|
98
163
|
const npxPath = findNpxPath();
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
164
|
+
must(run('schtasks', windowsCreateArgs(npxPath)), 'schtasks /Create');
|
|
165
|
+
// Check what was registered rather than trusting the exit code alone.
|
|
166
|
+
const q = run('schtasks', ['/Query', '/TN', TASK_NAME, '/XML']);
|
|
167
|
+
if (q.status === 0 && !q.stdout.includes('crbro-memory mine')) {
|
|
168
|
+
throw new Error('the task was created but its command does not run the miner');
|
|
169
|
+
}
|
|
104
170
|
return {
|
|
105
171
|
success: true,
|
|
106
172
|
platform: 'windows',
|
|
@@ -109,7 +175,7 @@ function setupWindows() {
|
|
|
109
175
|
message: `✅ CRBRO Auto-Miner scheduled!\n\n` +
|
|
110
176
|
` Task Name: ${TASK_NAME}\n` +
|
|
111
177
|
` Frequency: Every ${FREQUENCY_HOURS} hours\n` +
|
|
112
|
-
` Command:
|
|
178
|
+
` Command: ${windowsTaskCommand(npxPath)}\n\n` +
|
|
113
179
|
` To check status: npx crbro-memory miner-status\n` +
|
|
114
180
|
` To remove: npx crbro-memory remove-miner`,
|
|
115
181
|
uninstall_command: `schtasks /Delete /TN "${TASK_NAME}" /F`,
|
|
@@ -121,13 +187,13 @@ function setupWindows() {
|
|
|
121
187
|
platform: 'windows',
|
|
122
188
|
method: 'Task Scheduler (schtasks)',
|
|
123
189
|
frequency: `${FREQUENCY_HOURS}h`,
|
|
124
|
-
message: `Failed to create scheduled task: ${err instanceof Error ? err.message : String(err)}.\n\nTry running as Administrator, or create the task manually:\n schtasks /Create /TN "${TASK_NAME}" /TR "npx -y crbro-memory mine" /SC HOURLY /MO ${FREQUENCY_HOURS}`,
|
|
190
|
+
message: `Failed to create scheduled task: ${err instanceof Error ? err.message : String(err)}.\n\nTry running as Administrator, or create the task manually:\n schtasks /Create /TN "${TASK_NAME}" /TR "\\"<path to npx.cmd>\\" -y crbro-memory mine" /SC HOURLY /MO ${FREQUENCY_HOURS}`,
|
|
125
191
|
};
|
|
126
192
|
}
|
|
127
193
|
}
|
|
128
194
|
function getWindowsStatus() {
|
|
129
195
|
try {
|
|
130
|
-
const output = (
|
|
196
|
+
const output = must(run('schtasks', ['/Query', '/TN', TASK_NAME, '/FO', 'CSV', '/NH']), 'schtasks /Query').stdout;
|
|
131
197
|
const parts = output.trim().split(',').map(s => s.replace(/"/g, ''));
|
|
132
198
|
return {
|
|
133
199
|
installed: true,
|
|
@@ -151,32 +217,32 @@ function getWindowsStatus() {
|
|
|
151
217
|
async function setupMacOS() {
|
|
152
218
|
const plistPath = (0, path_1.join)((0, os_1.homedir)(), 'Library', 'LaunchAgents', 'com.crbro.miner.plist');
|
|
153
219
|
const npxPath = findNpxPath();
|
|
154
|
-
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
155
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
156
|
-
<plist version="1.0">
|
|
157
|
-
<dict>
|
|
158
|
-
<key>Label</key>
|
|
159
|
-
<string>com.crbro.miner</string>
|
|
160
|
-
<key>ProgramArguments</key>
|
|
161
|
-
<array>
|
|
162
|
-
<string>${npxPath}</string>
|
|
163
|
-
<string>-y</string>
|
|
164
|
-
<string>crbro-memory</string>
|
|
165
|
-
<string>mine</string>
|
|
166
|
-
</array>
|
|
167
|
-
<key>StartInterval</key>
|
|
168
|
-
<integer>${FREQUENCY_HOURS * 3600}</integer>
|
|
169
|
-
<key>RunAtLoad</key>
|
|
170
|
-
<true/>
|
|
171
|
-
<key>StandardOutPath</key>
|
|
172
|
-
<string>${(0, path_1.join)((
|
|
173
|
-
<key>StandardErrorPath</key>
|
|
174
|
-
<string>${(0, path_1.join)((
|
|
175
|
-
</dict>
|
|
220
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
221
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
222
|
+
<plist version="1.0">
|
|
223
|
+
<dict>
|
|
224
|
+
<key>Label</key>
|
|
225
|
+
<string>com.crbro.miner</string>
|
|
226
|
+
<key>ProgramArguments</key>
|
|
227
|
+
<array>
|
|
228
|
+
<string>${xmlEscape(npxPath)}</string>
|
|
229
|
+
<string>-y</string>
|
|
230
|
+
<string>crbro-memory</string>
|
|
231
|
+
<string>mine</string>
|
|
232
|
+
</array>
|
|
233
|
+
<key>StartInterval</key>
|
|
234
|
+
<integer>${FREQUENCY_HOURS * 3600}</integer>
|
|
235
|
+
<key>RunAtLoad</key>
|
|
236
|
+
<true/>
|
|
237
|
+
<key>StandardOutPath</key>
|
|
238
|
+
<string>${xmlEscape((0, path_1.join)(brainRoot(), 'miner.log'))}</string>
|
|
239
|
+
<key>StandardErrorPath</key>
|
|
240
|
+
<string>${xmlEscape((0, path_1.join)(brainRoot(), 'miner_error.log'))}</string>
|
|
241
|
+
</dict>
|
|
176
242
|
</plist>`;
|
|
177
243
|
try {
|
|
178
244
|
await (0, promises_1.writeFile)(plistPath, plistContent, 'utf-8');
|
|
179
|
-
(
|
|
245
|
+
must(run('launchctl', ['load', plistPath]), 'launchctl load');
|
|
180
246
|
return {
|
|
181
247
|
success: true,
|
|
182
248
|
platform: 'macos',
|
|
@@ -215,20 +281,29 @@ function getMacOSStatus() {
|
|
|
215
281
|
};
|
|
216
282
|
}
|
|
217
283
|
// ─── Linux: Crontab ───────────────────────────────────────────
|
|
218
|
-
|
|
284
|
+
/** cron runs the line through /bin/sh: paths are single-quoted for it. */
|
|
285
|
+
function shQuote(s) {
|
|
286
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
287
|
+
}
|
|
288
|
+
function xmlEscape(s) {
|
|
289
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
290
|
+
}
|
|
291
|
+
function linuxCronLine(npxPath, logPath) {
|
|
292
|
+
return `0 */${FREQUENCY_HOURS} * * * ${shQuote(npxPath)} -y crbro-memory mine >> ${shQuote(logPath)} 2>&1`;
|
|
293
|
+
}
|
|
294
|
+
async function setupLinux() {
|
|
219
295
|
try {
|
|
220
296
|
const npxPath = findNpxPath();
|
|
221
|
-
const cronLine =
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const filtered = currentCron.split('\n').filter(l => !l.includes('crbro-memory mine'));
|
|
297
|
+
const cronLine = linuxCronLine(npxPath, (0, path_1.join)(brainRoot(), 'miner.log'));
|
|
298
|
+
// Current crontab, our old line out, the new one in. The user's own lines
|
|
299
|
+
// are kept byte for byte, blank lines and comments included.
|
|
300
|
+
const currentCron = readCrontab();
|
|
301
|
+
const filtered = cronWithout(currentCron);
|
|
302
|
+
while (filtered.length && !filtered[filtered.length - 1].trim())
|
|
303
|
+
filtered.pop();
|
|
229
304
|
filtered.push(cronLine);
|
|
230
|
-
const newCron = filtered.
|
|
231
|
-
(
|
|
305
|
+
const newCron = filtered.join('\n') + '\n';
|
|
306
|
+
await writeCrontab(currentCron, newCron);
|
|
232
307
|
return {
|
|
233
308
|
success: true,
|
|
234
309
|
platform: 'linux',
|
|
@@ -237,7 +312,8 @@ function setupLinux() {
|
|
|
237
312
|
message: `✅ CRBRO Auto-Miner scheduled!\n\n` +
|
|
238
313
|
` Cron: ${cronLine}\n` +
|
|
239
314
|
` Frequency: Every ${FREQUENCY_HOURS} hours\n` +
|
|
240
|
-
` Log:
|
|
315
|
+
` Log: ${(0, path_1.join)(brainRoot(), 'miner.log')}\n` +
|
|
316
|
+
` The previous crontab was saved next to the brain as crontab.before-crbro-*.txt.\n\n` +
|
|
241
317
|
` To check: npx crbro-memory miner-status\n` +
|
|
242
318
|
` To remove: npx crbro-memory remove-miner`,
|
|
243
319
|
uninstall_command: 'npx crbro-memory remove-miner',
|
|
@@ -255,7 +331,7 @@ function setupLinux() {
|
|
|
255
331
|
}
|
|
256
332
|
function getLinuxStatus() {
|
|
257
333
|
try {
|
|
258
|
-
const cron = (
|
|
334
|
+
const cron = readCrontab();
|
|
259
335
|
const hasCrbro = cron.includes('crbro-memory mine');
|
|
260
336
|
return {
|
|
261
337
|
installed: hasCrbro,
|
|
@@ -279,15 +355,15 @@ function getLinuxStatus() {
|
|
|
279
355
|
}
|
|
280
356
|
// ─── Utilities ────────────────────────────────────────────────
|
|
281
357
|
function findNpxPath() {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
catch {
|
|
290
|
-
return 'npx'; // Fallback to PATH
|
|
358
|
+
const os = currentPlatform();
|
|
359
|
+
if (os === 'win32') {
|
|
360
|
+
// `where npx` lists the extensionless sh script first; Task Scheduler
|
|
361
|
+
// needs the .cmd shim.
|
|
362
|
+
const r = run('where', ['npx.cmd']);
|
|
363
|
+
const first = r.status === 0 ? r.stdout.split(/\r?\n/).map(l => l.trim()).find(l => /\.cmd$/i.test(l)) : undefined;
|
|
364
|
+
return first || 'npx.cmd';
|
|
291
365
|
}
|
|
366
|
+
const r = run('which', ['npx']);
|
|
367
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim().split('\n')[0] : 'npx';
|
|
292
368
|
}
|
|
293
369
|
//# sourceMappingURL=scheduler.js.map
|