@bcmyguest/openmem 0.0.0-stage → 0.6.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/LICENSE +190 -0
- package/README.md +136 -2
- package/bin/hindsight-project-index +530 -0
- package/cli/coding-agent/access-policy.mjs +38 -0
- package/cli/coding-agent/agents.mjs +34 -0
- package/cli/coding-agent/bank-from-cwd.mjs +140 -0
- package/cli/coding-agent/capabilities.mjs +23 -0
- package/cli/coding-agent/hook-harnesses.mjs +87 -0
- package/cli/coding-agent/hook-runner.mjs +260 -0
- package/cli/coding-agent/install-access.mjs +93 -0
- package/cli/coding-agent/install-shim.mjs +97 -0
- package/cli/coding-agent/opencode-config.mjs +91 -0
- package/cli/coding-agent/opencode-plugin.mjs +96 -0
- package/cli/coding-agent/runtime.mjs +39 -0
- package/cli/coding-agent/session-facade.mjs +62 -0
- package/cli/cognee-mcp.mjs +260 -0
- package/cli/feed-sync.mjs +11 -0
- package/cli/install-coding-agent.mjs +165 -0
- package/cli/install-memory-mcp.mjs +426 -0
- package/cli/is-main.mjs +18 -0
- package/cli/mcp/context.mjs +61 -0
- package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
- package/cli/mcp/export/cognee.mjs +119 -0
- package/cli/mcp/export/hindsight.mjs +48 -0
- package/cli/mcp/export/index.mjs +26 -0
- package/cli/mcp/export/mem0.mjs +31 -0
- package/cli/mcp/llm-synthesize.mjs +50 -0
- package/cli/mcp/mem0-scope-id.mjs +22 -0
- package/cli/mcp/memory-facade.mjs +455 -0
- package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
- package/cli/mcp/notebooklm-banks.mjs +21 -0
- package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
- package/cli/mcp/notebooklm-provider.mjs +225 -0
- package/cli/mcp/pmm-records.mjs +23 -0
- package/cli/mcp/portability.mjs +158 -0
- package/cli/mcp/stdio.mjs +65 -0
- package/cli/mcp/unified-mcp-install.mjs +65 -0
- package/cli/mem0-mcp.mjs +108 -0
- package/cli/notebooklm-mcp.mjs +236 -0
- package/cli/openmem-mcp.mjs +35 -0
- package/cli/openmem.mjs +205 -0
- package/cli/roots.mjs +85 -0
- package/cli/stack.mjs +436 -0
- package/cli/switch-memory-provider.mjs +358 -0
- package/cli/wiring-report.mjs +125 -0
- package/compose.bench.yaml +12 -0
- package/compose.yaml +164 -0
- package/config/coding-agent.json +13 -0
- package/config/hindsight.env.example +24 -0
- package/config/index.yml +54 -0
- package/config/litellm.lemond.yaml +40 -0
- package/config/litellm.ollama.yaml +33 -0
- package/config/notebooklm.yml +33 -0
- package/config/openmem.toml.example +62 -0
- package/docs/OPENMEM.md +23 -0
- package/docs/architecture.md +219 -0
- package/docs/assets/README.md +10 -0
- package/docs/assets/setup-demo.gif +0 -0
- package/docs/coding-agent-hooks.md +51 -0
- package/docs/coding-agent-scopes.md +22 -0
- package/docs/coding-agents.md +51 -0
- package/docs/cognee.md +37 -0
- package/docs/concepts.md +112 -0
- package/docs/hindsight.md +74 -0
- package/docs/index-sources-roadmap.md +38 -0
- package/docs/index-sources.md +39 -0
- package/docs/indexer-sinks.md +122 -0
- package/docs/indexing.md +57 -0
- package/docs/manual-stack-setup.md +74 -0
- package/docs/mcp-install-guide.md +4 -0
- package/docs/mcp-setup.md +101 -0
- package/docs/mem0.md +103 -0
- package/docs/memory-portability.md +73 -0
- package/docs/memory-providers.md +246 -0
- package/docs/model-configuration.md +84 -0
- package/docs/notebooklm.md +83 -0
- package/docs/npx-install.md +170 -0
- package/docs/openmem-config.md +194 -0
- package/docs/provider-wizard-hooks.md +222 -0
- package/docs/quickstart.md +101 -0
- package/docs/setup-wizard.md +116 -0
- package/docs/sources-feeds.md +52 -0
- package/docs/sources-readwise.md +23 -0
- package/docs/sources-zotero.md +22 -0
- package/glama.json +4 -0
- package/indexer/Dockerfile +38 -0
- package/indexer/code_outline.py +146 -0
- package/indexer/feed_sync.py +107 -0
- package/indexer/index_config.py +290 -0
- package/indexer/pytest.ini +5 -0
- package/indexer/sinks/__init__.py +148 -0
- package/indexer/sinks/_http.py +44 -0
- package/indexer/sinks/cognee.py +329 -0
- package/indexer/sinks/hindsight.py +107 -0
- package/indexer/sinks/mem0.py +247 -0
- package/indexer/watch.py +240 -0
- package/package.json +95 -4
- package/providers/_template/compose.yaml +52 -0
- package/providers/cognee/OPERATOR.md +82 -0
- package/providers/cognee/compose.yaml +89 -0
- package/providers/cognee/lib/clack.mjs +157 -0
- package/providers/cognee/lib/coding-agent.mjs +26 -0
- package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
- package/providers/cognee/lib/config.mjs +98 -0
- package/providers/cognee/lib/env-file.mjs +194 -0
- package/providers/cognee/lib/env-setup.mjs +83 -0
- package/providers/cognee/lib/llm-env.mjs +382 -0
- package/providers/cognee/lib/ports.mjs +25 -0
- package/providers/cognee/lib/smoke.mjs +246 -0
- package/providers/cognee/lib/stack-env.mjs +134 -0
- package/providers/cognee/lib/state.mjs +101 -0
- package/providers/cognee/lib/summary.mjs +56 -0
- package/providers/cognee/lib/validate.mjs +97 -0
- package/providers/cognee/manifest.json +56 -0
- package/providers/cognee/wizard.mjs +134 -0
- package/providers/graphiti/manifest.json +15 -0
- package/providers/hindsight/lib/clack.mjs +245 -0
- package/providers/hindsight/lib/coding-agent.mjs +204 -0
- package/providers/hindsight/lib/config.mjs +73 -0
- package/providers/hindsight/lib/env-setup.mjs +71 -0
- package/providers/hindsight/lib/litellm-routes.mjs +187 -0
- package/providers/hindsight/lib/model-backend.mjs +214 -0
- package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
- package/providers/hindsight/lib/ollama-compose.mjs +51 -0
- package/providers/hindsight/lib/ports.mjs +30 -0
- package/providers/hindsight/lib/probe.mjs +33 -0
- package/providers/hindsight/lib/stack-env.mjs +132 -0
- package/providers/hindsight/lib/state.mjs +75 -0
- package/providers/hindsight/lib/summary.mjs +43 -0
- package/providers/hindsight/lib/validate.mjs +75 -0
- package/providers/hindsight/manifest.json +58 -0
- package/providers/hindsight/wizard.mjs +149 -0
- package/providers/letta/manifest.json +29 -0
- package/providers/mem0/OPERATOR.md +124 -0
- package/providers/mem0/compose.yaml +57 -0
- package/providers/mem0/lib/coding-agent.mjs +58 -0
- package/providers/mem0/lib/config.mjs +45 -0
- package/providers/mem0/lib/configure.mjs +135 -0
- package/providers/mem0/lib/dotenv.mjs +71 -0
- package/providers/mem0/lib/env-file.mjs +110 -0
- package/providers/mem0/lib/http.mjs +69 -0
- package/providers/mem0/lib/mcp-install.mjs +165 -0
- package/providers/mem0/lib/secret.mjs +30 -0
- package/providers/mem0/lib/smoke.mjs +179 -0
- package/providers/mem0/lib/stack-env.mjs +216 -0
- package/providers/mem0/lib/state.mjs +67 -0
- package/providers/mem0/lib/summary.mjs +60 -0
- package/providers/mem0/lib/url.mjs +106 -0
- package/providers/mem0/lib/validate.mjs +158 -0
- package/providers/mem0/manifest.json +47 -0
- package/providers/mem0/wizard.mjs +133 -0
- package/providers/zep/manifest.json +29 -0
- package/scripts/check-pack.mjs +218 -0
- package/scripts/gen-masks.mjs +83 -0
- package/scripts/gen-masks.sh +4 -0
- package/scripts/promotion-provider-smoke.mjs +34 -0
- package/scripts/run-setup.sh +30 -0
- package/scripts/setup.mjs +94 -0
- package/scripts/setup.sh +5 -0
- package/scripts/spin-up.sh +45 -0
- package/scripts/wizard.sh +5 -0
- package/server.json +34 -0
- package/skills/openmem/SKILL.md +29 -0
- package/sync/Dockerfile +27 -0
- package/sync/memory_backend.py +87 -0
- package/sync/notebooklm_sync.py +664 -0
- package/wizard/bin/pm.mjs +97 -0
- package/wizard/bin/setup.mjs +33 -0
- package/wizard/lib/compose-version.mjs +32 -0
- package/wizard/lib/compose.mjs +70 -0
- package/wizard/lib/curl.mjs +103 -0
- package/wizard/lib/docker-build.mjs +148 -0
- package/wizard/lib/exec.mjs +41 -0
- package/wizard/lib/indexer.mjs +6 -0
- package/wizard/lib/load-machine-env.mjs +4 -0
- package/wizard/lib/log.mjs +32 -0
- package/wizard/lib/notebooklm-agent-access.mjs +92 -0
- package/wizard/lib/notebooklm.mjs +214 -0
- package/wizard/lib/parse-args.mjs +87 -0
- package/wizard/lib/paths.mjs +39 -0
- package/wizard/lib/pm-config.mjs +163 -0
- package/wizard/lib/ports.mjs +94 -0
- package/wizard/lib/prompts.mjs +57 -0
- package/wizard/lib/provider-wizard/helpers.mjs +98 -0
- package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
- package/wizard/lib/providers/registry.mjs +262 -0
- package/wizard/lib/run-clack.mjs +108 -0
- package/wizard/lib/run-wizard.mjs +755 -0
- package/wizard/lib/validate-setup.mjs +62 -0
- package/wizard/lib/wizard-action.mjs +34 -0
- package/wizard/lib/wizard-conf.mjs +71 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
Get from zero to a running stack in one wizard run. **Choose your memory provider** in the wizard (Hindsight by default; mem0 or Cognee if you prefer) — same CLI either way. Cloning the repo is optional; **`npx @bcmyguest/openmem`** is the usual path. For why OpenMem exists (interface layer, not a memory engine), see [Concepts](concepts.md).
|
|
4
|
+
|
|
5
|
+
## Before you start
|
|
6
|
+
|
|
7
|
+
| Requirement | Why |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Docker + Compose v2 | Runs the stack; your user must run `docker` without sudo |
|
|
10
|
+
| Node.js ≥ 20.12 | CLI and setup wizard |
|
|
11
|
+
| LLM for Hindsight (default provider) | lemond, Ollama, or OpenAI-compatible — wizard wires LiteLLM |
|
|
12
|
+
| git, curl | Wizard preflight |
|
|
13
|
+
|
|
14
|
+
Default stack is [Hindsight](hindsight.md). Other providers: wizard or `npx @bcmyguest/openmem providers` ([provider status](provider-status.md)).
|
|
15
|
+
|
|
16
|
+
## 1. Run the wizard
|
|
17
|
+
|
|
18
|
+
**From npm (recommended):**
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx @bcmyguest/openmem
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**From a git checkout** (repo directory is the stack root):
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
git clone https://github.com/bcmyguest/openmem.git
|
|
28
|
+
cd openmem
|
|
29
|
+
npm install
|
|
30
|
+
npx . setup
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The wizard asks for a memory provider, provider settings, what to index, then starts Docker and runs a smoke test.
|
|
34
|
+
|
|
35
|
+
Useful variants:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npx @bcmyguest/openmem setup --dry-run # plan only, no writes
|
|
39
|
+
npx @bcmyguest/openmem configure --yes # defaults + start, no prompts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Wizard dry-run (what first setup plans — no writes):
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
## 2. Confirm it is up
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npx @bcmyguest/openmem doctor
|
|
50
|
+
curl -fsS http://127.0.0.1:8888/health # Hindsight default; see wizard output for other providers
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 3. Wire your coding agent (optional)
|
|
54
|
+
|
|
55
|
+
**Session memory:** `npx @bcmyguest/openmem install-coding-agent --yes` — unified MCP **plus** session hooks for Claude Code, Codex, OpenCode, Cursor CLI (any active provider). Optional `--hindsight-plugin` adds the upstream Hindsight plugin. Not the same as `install-mcp` alone.
|
|
56
|
+
|
|
57
|
+
**`install-mcp`** merges the **`openmem` unified MCP** (`memory_add`, `memory_search`, `memory_recall`, `memory_reflect`, `memory_consolidate`, `memory_forget` on whichever provider you picked) and, when NotebookLM sync is on, the **NotebookLM MCP**:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npx @bcmyguest/openmem install-mcp --yes
|
|
61
|
+
npx @bcmyguest/openmem install-mcp --yes --notebooklm # force NotebookLM entry
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Session hooks use the in-repo shim by default; add `--hindsight-plugin` if you also want the upstream Hindsight plugin. Optional native mem0/Cognee MCP: `install-mcp --provider-only`. Full guide: [MCP setup](mcp-setup.md).
|
|
65
|
+
|
|
66
|
+
Restart Cursor Agent after MCP changes. Move memories between providers with [Portable memory (PMM)](memory-portability.md) — `switch` alone does not migrate data.
|
|
67
|
+
|
|
68
|
+
## Day-2
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npx @bcmyguest/openmem up
|
|
72
|
+
npx @bcmyguest/openmem down
|
|
73
|
+
npx @bcmyguest/openmem switch mem0 --yes
|
|
74
|
+
npx @bcmyguest/openmem doctor
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Command | Purpose |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `up` / `down` | Start or stop the stack (down keeps data volumes) |
|
|
80
|
+
| `switch <id> --yes` | Change provider (no memory migration) |
|
|
81
|
+
| `doctor` | Validate config without the smoke test |
|
|
82
|
+
|
|
83
|
+
In a checkout, use `npx .` instead of `npx @bcmyguest/openmem`.
|
|
84
|
+
|
|
85
|
+
## Where files live
|
|
86
|
+
|
|
87
|
+
| Install | Stack root (compose, `.env`) |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `npx @bcmyguest/openmem` | `~/.local/share/personal-memory/stack` (override: `PM_STACK_ROOT`) |
|
|
90
|
+
| Git checkout | The clone directory |
|
|
91
|
+
|
|
92
|
+
Secrets and index config: `~/.config/hindsight/` (and optional `~/.config/personal-memory/config.toml`).
|
|
93
|
+
|
|
94
|
+
## Next
|
|
95
|
+
|
|
96
|
+
- [MCP setup](mcp-setup.md) — unified `memory_*`, provider-native, NotebookLM
|
|
97
|
+
- [Portable memory (PMM)](memory-portability.md) — export/import
|
|
98
|
+
- [Architecture](architecture.md) — data flow, indexing, MCP by provider
|
|
99
|
+
- [Index sources](index-sources.md) — git, Obsidian, notes/papers/bookmarks, feed cache
|
|
100
|
+
- [Install with npx](npx-install.md) — upgrades, troubleshooting
|
|
101
|
+
- [Configuration file](openmem-config.md) — declarative `config.toml`
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Setup wizard
|
|
2
|
+
|
|
3
|
+
The wizard is a Node program (`wizard/bin/setup.mjs`). It is exposed as:
|
|
4
|
+
|
|
5
|
+
| How you run it | When |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `npx @bcmyguest/openmem` / `npx @bcmyguest/openmem setup` | Normal install ([Install with npx](npx-install.md)) |
|
|
8
|
+
| `npx . setup` / `npm run setup` / `./setup` | Git checkout (stack root stays in the repo) |
|
|
9
|
+
|
|
10
|
+
Interactive runs use [@clack/prompts](https://github.com/bombshell-dev/clack). `--yes` and `--dry-run` skip Clack.
|
|
11
|
+
|
|
12
|
+
## Stack actions
|
|
13
|
+
|
|
14
|
+
Before provider questions, pick what the run should do (Clack or `PM_WIZARD_ACTION`):
|
|
15
|
+
|
|
16
|
+
| Action | Flag | Behavior |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Configure | default | Full wizard: provider, env, index sources, start stack, optional coding-agent step |
|
|
19
|
+
| Start | `--up` | Requires existing `~/.config/hindsight/wizard.conf`; starts stack and validates |
|
|
20
|
+
| Stop | `--down` | `docker compose down` for the project |
|
|
21
|
+
|
|
22
|
+
Non-interactive shortcuts:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npx @bcmyguest/openmem configure --yes # setup --yes
|
|
26
|
+
npx @bcmyguest/openmem up # wizard --up (after first configure)
|
|
27
|
+
npx @bcmyguest/openmem down # wizard --down
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Wizard phases
|
|
31
|
+
|
|
32
|
+
Configure runs these steps in order (provider hooks replace or extend some steps):
|
|
33
|
+
|
|
34
|
+
| Step | Title | What happens |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| 1 | Preflight | `git`, `curl`, `docker`, Compose v2; host UID/GID; API/control-plane ports |
|
|
37
|
+
| 2 | Memory provider | Hindsight, mem0, or Cognee ([Provider status](provider-status.md)) |
|
|
38
|
+
| 3 | Provider config | See table below |
|
|
39
|
+
| 4 | Secrets and env | Files under `~/.config/hindsight/` (mode 600); runs `scripts/setup.mjs` for stack `.env` and `compose.override.yaml` |
|
|
40
|
+
| 5 | Sources to index | Writes `~/.config/hindsight/index.yml`; lists sources with a dry indexer run |
|
|
41
|
+
| 6 | NotebookLM sync | Opt-in; Google login when enabled |
|
|
42
|
+
| 7 | Start the stack | Build indexer image if inputs changed; health wait; smoke retain/recall (unless `--no-smoke`) |
|
|
43
|
+
| 8 | Coding-agent plugin | Optional `npx` install for detected CLIs; provider-specific MCP hints |
|
|
44
|
+
| 9 | Validate memory setup | Provider checks + shared validation |
|
|
45
|
+
|
|
46
|
+
The last line is `wizard: changed=N` (files and containers this run touched). An unchanged re-run prints `changed=0`.
|
|
47
|
+
|
|
48
|
+
## Step 3 by provider
|
|
49
|
+
|
|
50
|
+
| Provider | Step 3 asks for |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| Hindsight | Model backend: lemond, Ollama (host or compose profile), or custom OpenAI-compatible URL; LiteLLM routes for `memory-retain`, `memory-reflect`, `memory-consolidate` |
|
|
53
|
+
| mem0 | Self-hosted REST base URL and API key file path |
|
|
54
|
+
| Cognee | Loopback port and `LLM_API_KEY` for graph build |
|
|
55
|
+
|
|
56
|
+
Details: [Model configuration](model-configuration.md) (Hindsight), `providers/mem0/OPERATOR.md`, `providers/cognee/OPERATOR.md`.
|
|
57
|
+
|
|
58
|
+
## Where configuration lives
|
|
59
|
+
|
|
60
|
+
| Path | Written by | Purpose |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| Stack root `.env`, `.build-hash`, `compose.override.yaml` | `scripts/setup.mjs` | Compose project; indexer UID; secret-path masks |
|
|
63
|
+
| `~/.config/hindsight/wizard.conf` | Wizard | Provider id, backend choice, ports |
|
|
64
|
+
| `~/.config/hindsight/hindsight.env`, `litellm.env` | Setup / Hindsight hooks | Hindsight + LiteLLM secrets |
|
|
65
|
+
| `~/.config/hindsight/index.yml` | Wizard step 5 | Index roots, repos, vaults; optional `notes` / `papers` / `bookmarks` / `feeds` ([index-sources.md](index-sources.md)) |
|
|
66
|
+
| `~/.config/hindsight/notebooklm.yml` | Wizard step 6 | NotebookLM sync toggles |
|
|
67
|
+
| `~/.config/personal-memory/config.toml` | Optional; synced on configure | Declarative overrides ([Configuration file](openmem-config.md)) |
|
|
68
|
+
| `~/.hindsight/coding-agent.json` | Wizard / install-mcp | **Real file on disk** — do not symlink to the repo template |
|
|
69
|
+
|
|
70
|
+
### Stack root (npx installs)
|
|
71
|
+
|
|
72
|
+
When the CLI is not running from a git checkout, mutable compose files live under:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
${PM_STACK_ROOT:-${XDG_DATA_HOME:-~/.local/share}/personal-memory/stack}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Wizard code and provider hooks still run from the npm package. See [Install with npx](npx-install.md#stack-root).
|
|
79
|
+
|
|
80
|
+
## Wizard flags
|
|
81
|
+
|
|
82
|
+
| Flag | Effect |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `--yes` / `-y` | Accept defaults; no prompts |
|
|
85
|
+
| `--dry-run` | Plan only; read-only checks; no writes or containers |
|
|
86
|
+
| `--no-smoke` | Skip retain/recall smoke test |
|
|
87
|
+
| `--rebuild` | Rebuild indexer image even if sources unchanged |
|
|
88
|
+
| `--up` / `--down` | Start or stop only (see stack actions) |
|
|
89
|
+
|
|
90
|
+
Environment (common):
|
|
91
|
+
|
|
92
|
+
| Variable | Meaning |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `MEMORY_PROVIDER` | Default provider id |
|
|
95
|
+
| `PM_BACKEND` | Hindsight backend default (`lemond`, `ollama`, `custom`) |
|
|
96
|
+
| `PM_PROJECT` | Docker Compose project name |
|
|
97
|
+
| `PM_STACK_ROOT` / `PM_USE_CHECKOUT` | Stack root location |
|
|
98
|
+
| `HINDSIGHT_PORT`, `HINDSIGHT_CP_PORT` | Host API and control-plane ports |
|
|
99
|
+
|
|
100
|
+
Run `./setup --help` in a checkout for the full list.
|
|
101
|
+
|
|
102
|
+
## Indexer image builds
|
|
103
|
+
|
|
104
|
+
The wizard rebuilds the indexer image only when its inputs change (Dockerfile, `compose.yaml`, copied sources). The hash is stored in `.build-hash` at the stack root. If nothing changed, it uses `up -d --no-build` so containers are not recreated unnecessarily.
|
|
105
|
+
|
|
106
|
+
## Related docs
|
|
107
|
+
|
|
108
|
+
| Topic | Doc |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| npx, stack root, upgrades | [npx-install.md](npx-install.md) |
|
|
111
|
+
| `config.toml` and day-2 tasks | [openmem-config.md](openmem-config.md) |
|
|
112
|
+
| Manual compose without the wizard | [manual-stack-setup.md](manual-stack-setup.md) |
|
|
113
|
+
| LiteLLM routes and models | [model-configuration.md](model-configuration.md) |
|
|
114
|
+
| Index sources and `index.yml` | [indexing.md](indexing.md) |
|
|
115
|
+
| NotebookLM sync and MCP | [notebooklm.md](notebooklm.md) |
|
|
116
|
+
| Plugins and MCP install | [coding-agents.md](coding-agents.md) |
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# RSS and OpenAlex feeds
|
|
2
|
+
|
|
3
|
+
Configure `feeds` in `index.yml`, run sync, then the indexer watches `feeds.out_dir` as kind **feed** (`feed::*` banks).
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
feeds:
|
|
7
|
+
out_dir: ~/.local/share/personal-memory/feeds
|
|
8
|
+
rss:
|
|
9
|
+
- url: https://rss.arxiv.org/rss/cs.AI
|
|
10
|
+
slug: arxiv-cs-ai
|
|
11
|
+
openalex_queries:
|
|
12
|
+
- url: https://api.openalex.org/works?search=machine%20learning&per_page=25
|
|
13
|
+
slug: openalex-ml
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx @bcmyguest/openmem feed-sync
|
|
18
|
+
# checkout: npm run feed-sync
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then ensure the stack indexer is running. Each run writes markdown files under `out_dir`; the watcher indexes new/changed files.
|
|
22
|
+
|
|
23
|
+
Network use: feed-sync calls public RSS/OpenAlex URLs from the host (not from CI by default).
|
|
24
|
+
|
|
25
|
+
## Cron / timer (optional)
|
|
26
|
+
|
|
27
|
+
Run sync on a schedule so feeds stay fresh without manual commands:
|
|
28
|
+
|
|
29
|
+
```ini
|
|
30
|
+
# ~/.config/systemd/user/openmem-feed-sync.timer
|
|
31
|
+
[Unit]
|
|
32
|
+
Description=Refresh openmem feed cache
|
|
33
|
+
|
|
34
|
+
[Timer]
|
|
35
|
+
OnCalendar=hourly
|
|
36
|
+
Persistent=true
|
|
37
|
+
|
|
38
|
+
[Install]
|
|
39
|
+
WantedBy=timers.target
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ini
|
|
43
|
+
# ~/.config/systemd/user/openmem-feed-sync.service
|
|
44
|
+
[Unit]
|
|
45
|
+
Description=openmem feed-sync
|
|
46
|
+
|
|
47
|
+
[Service]
|
|
48
|
+
Type=oneshot
|
|
49
|
+
ExecStart=/usr/bin/env npx @bcmyguest/openmem feed-sync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Enable with `systemctl --user enable --now openmem-feed-sync.timer`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Readwise → indexer
|
|
2
|
+
|
|
3
|
+
Export highlights from [Readwise Reader](https://readwise.io/read) or the legacy export as **Markdown**.
|
|
4
|
+
|
|
5
|
+
1. Export to a directory (Readwise → Export → Markdown).
|
|
6
|
+
2. Add to `notes.paths` or `papers.paths` depending on how you split reading vs research:
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
notes:
|
|
10
|
+
paths:
|
|
11
|
+
- ~/Readwise/Full-Sync
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
3. Re-run indexing; chunks retain with bank `notes::full-sync` (slug from folder name).
|
|
15
|
+
|
|
16
|
+
Optional stable bank id:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
banks:
|
|
20
|
+
~/Readwise/Full-Sync: readwise::full-sync
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For automatic sync, use Readwise’s scheduled export + the same path (filesystem watcher picks up changes).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Zotero → indexer
|
|
2
|
+
|
|
3
|
+
**Assumption:** use export-to-disk first; no Zotero API key in the stack yet.
|
|
4
|
+
|
|
5
|
+
1. Install [Better BibTeX](https://retorque.re/zotero-better-bibtex/) (optional) for stable cite keys.
|
|
6
|
+
2. Export markdown or JSON notes to a folder, e.g. `~/Zotero/export-md`.
|
|
7
|
+
3. Point `papers.paths` at that folder in `~/.config/hindsight/index.yml`:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
papers:
|
|
11
|
+
paths:
|
|
12
|
+
- ~/Zotero/export-md
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
4. Restart the indexer container or run `bin/hindsight-project-index --apply --papers ~/Zotero/export-md`.
|
|
16
|
+
|
|
17
|
+
Banks default to `papers::<folder-name>` unless overridden under `banks:`:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
banks:
|
|
21
|
+
~/Zotero/export-md: papers::zotero
|
|
22
|
+
```
|
package/glama.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Project indexer: one pass at startup, then re-index each source after it stops changing.
|
|
2
|
+
FROM python:3.12-slim
|
|
3
|
+
|
|
4
|
+
RUN apt-get update \
|
|
5
|
+
&& apt-get install -y --no-install-recommends git ca-certificates \
|
|
6
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
7
|
+
|
|
8
|
+
# Tree-sitter grammars are prefetched here, so the runtime never downloads and can run with no network and a
|
|
9
|
+
# read-only $HOME. The directory is world-readable because the container runs as the host UID.
|
|
10
|
+
ENV TREE_SITTER_GRAMMAR_DIR=/opt/tree-sitter-grammars
|
|
11
|
+
RUN pip install --no-cache-dir watchfiles==1.3.0 pyyaml==6.0.3 tree-sitter-language-pack==1.22.1 \
|
|
12
|
+
&& python3 -c "import os; from tree_sitter_language_pack import PackConfig, download, init; \
|
|
13
|
+
init(PackConfig(cache_dir=os.environ['TREE_SITTER_GRAMMAR_DIR'])); \
|
|
14
|
+
download(['python', 'javascript', 'typescript', 'tsx', 'go', 'rust', 'java', 'c', 'cpp', 'csharp', 'ruby', 'lua', 'bash', 'php'])" \
|
|
15
|
+
&& chmod -R a+rX /opt/tree-sitter-grammars
|
|
16
|
+
|
|
17
|
+
# The compose user is the host's UID/GID, which may have no passwd entry here. Trust every repository owner, since
|
|
18
|
+
# the indexer only reads them and the owner is the host user.
|
|
19
|
+
RUN git config --system safe.directory '*'
|
|
20
|
+
|
|
21
|
+
COPY bin/hindsight-project-index /usr/local/bin/hindsight-project-index
|
|
22
|
+
COPY indexer/watch.py indexer/index_config.py indexer/code_outline.py indexer/feed_sync.py /opt/indexer/
|
|
23
|
+
# Memory sinks (one module per provider). A new indexer/sinks/<name>.py needs no Dockerfile edit.
|
|
24
|
+
COPY indexer/sinks/ /opt/indexer/sinks/
|
|
25
|
+
# Shipped default config. index_config.py finds it at ../config/index.yml relative to itself.
|
|
26
|
+
COPY config/index.yml /opt/config/index.yml
|
|
27
|
+
RUN chmod 0755 /usr/local/bin/hindsight-project-index /opt/indexer/watch.py \
|
|
28
|
+
&& chmod -R a+rX /opt/indexer
|
|
29
|
+
|
|
30
|
+
# $HOME is bind-mounted, so ignore the host's ~/.local site-packages (they would shadow the pinned packages).
|
|
31
|
+
# PYTHONPATH makes index_config, code_outline and the sinks package importable from both the indexer script and
|
|
32
|
+
# watch.py.
|
|
33
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
34
|
+
PYTHONNOUSERSITE=1 \
|
|
35
|
+
PYTHONPATH=/opt/indexer \
|
|
36
|
+
HINDSIGHT_URL=http://hindsight:8888
|
|
37
|
+
|
|
38
|
+
CMD ["python3", "/opt/indexer/watch.py"]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Compact symbol outlines for source files, parsed in-process with tree-sitter (tree-sitter-language-pack)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from tree_sitter_language_pack import PackConfig, ProcessConfig, get_parser, init, process
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SIGNATURE_LIMIT = 160
|
|
11
|
+
DOC_LIMIT = 120
|
|
12
|
+
MAX_OUTLINE_LINES = 150
|
|
13
|
+
MAX_IMPORTS = 60
|
|
14
|
+
GRAMMAR_DIR_ENV = "TREE_SITTER_GRAMMAR_DIR"
|
|
15
|
+
LANGUAGE_BY_SUFFIX = {
|
|
16
|
+
".py": "python",
|
|
17
|
+
".js": "javascript",
|
|
18
|
+
".jsx": "javascript",
|
|
19
|
+
".ts": "typescript",
|
|
20
|
+
".tsx": "tsx",
|
|
21
|
+
".go": "go",
|
|
22
|
+
".rs": "rust",
|
|
23
|
+
".java": "java",
|
|
24
|
+
".c": "c",
|
|
25
|
+
".h": "c",
|
|
26
|
+
".cc": "cpp",
|
|
27
|
+
".cpp": "cpp",
|
|
28
|
+
".hpp": "cpp",
|
|
29
|
+
".cs": "csharp",
|
|
30
|
+
".rb": "ruby",
|
|
31
|
+
".lua": "lua",
|
|
32
|
+
".sh": "bash",
|
|
33
|
+
".php": "php",
|
|
34
|
+
}
|
|
35
|
+
# The pack's structure extraction does not report imports for these grammars; match their import lines instead.
|
|
36
|
+
FALLBACK_IMPORTS = {
|
|
37
|
+
"c": re.compile(r"^\s*#\s*include\b"),
|
|
38
|
+
"cpp": re.compile(r"^\s*#\s*include\b"),
|
|
39
|
+
"csharp": re.compile(r"^\s*using\s+[\w.]+\s*;"),
|
|
40
|
+
"ruby": re.compile(r"^\s*require(_relative)?\b"),
|
|
41
|
+
"lua": re.compile(r"^\s*(local\s+[\w, ]+=\s*)?require\b"),
|
|
42
|
+
"php": re.compile(r"^\s*use\s+[\w\\ ,{}]+;"),
|
|
43
|
+
}
|
|
44
|
+
# Decorators and attributes sit above the definition; the signature is the first line after them.
|
|
45
|
+
ATTRIBUTE_PREFIXES = ("@", "#[", "[")
|
|
46
|
+
DOC_QUOTES = ('"""', "'''")
|
|
47
|
+
COMMENT_MARKERS = re.compile(r"^\s*(///<|///|//!|//|/\*\*|/\*!|/\*|\*|#|--)\s*")
|
|
48
|
+
|
|
49
|
+
_ready = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def language_for(path: str) -> str | None:
|
|
53
|
+
return LANGUAGE_BY_SUFFIX.get(Path(path).suffix.lower())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _load(language: str) -> None:
|
|
57
|
+
"""Load the grammar. Raises when it is missing, so a broken image fails the sync instead of emptying code docs."""
|
|
58
|
+
global _ready
|
|
59
|
+
if not _ready:
|
|
60
|
+
init(PackConfig(cache_dir=os.environ.get(GRAMMAR_DIR_ENV) or None))
|
|
61
|
+
_ready = True
|
|
62
|
+
get_parser(language)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _clip(text: str, limit: int = SIGNATURE_LIMIT) -> str:
|
|
66
|
+
return text if len(text) <= limit else text[: limit - 3] + "..."
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _one_line(text: str) -> str:
|
|
70
|
+
return " ".join(text.split())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _signature(item, lines: list[str]) -> str:
|
|
74
|
+
for row in range(item.span.start_line, min(item.span.end_line, len(lines) - 1) + 1):
|
|
75
|
+
text = lines[row].strip()
|
|
76
|
+
if text and not text.startswith(ATTRIBUTE_PREFIXES):
|
|
77
|
+
return _clip(text)
|
|
78
|
+
return _clip(f"{item.kind} {item.name or ''}".strip())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _doc(item, lines: list[str], language: str) -> str:
|
|
82
|
+
if language == "python" and item.body_span is not None:
|
|
83
|
+
# Python docstrings are the first statement of the body; the pack does not extract them.
|
|
84
|
+
for row in range(item.body_span.start_line, min(item.body_span.start_line + 2, len(lines))):
|
|
85
|
+
text = lines[row].strip()
|
|
86
|
+
if not text.startswith(DOC_QUOTES):
|
|
87
|
+
return ""
|
|
88
|
+
cleaned = text[3:].rstrip("\"'").strip()
|
|
89
|
+
if cleaned:
|
|
90
|
+
return _clip(cleaned, DOC_LIMIT)
|
|
91
|
+
return ""
|
|
92
|
+
for raw in (item.doc_comment or "").splitlines():
|
|
93
|
+
cleaned = COMMENT_MARKERS.sub("", raw).strip().rstrip("*/").strip()
|
|
94
|
+
if cleaned:
|
|
95
|
+
return _clip(cleaned, DOC_LIMIT)
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _imports(result, language: str, lines: list[str]) -> list[str]:
|
|
100
|
+
found = [_clip(_one_line(item.source)) for item in result.imports]
|
|
101
|
+
if not found and language in FALLBACK_IMPORTS:
|
|
102
|
+
found = [_clip(_one_line(line)) for line in lines if FALLBACK_IMPORTS[language].match(line)]
|
|
103
|
+
return list(dict.fromkeys(item for item in found if item))[:MAX_IMPORTS]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _symbol_lines(items, lines: list[str], language: str, depth: int, out: list[str], rows: set[int]) -> None:
|
|
107
|
+
indent = " " * depth
|
|
108
|
+
for item in items:
|
|
109
|
+
if len(out) >= MAX_OUTLINE_LINES:
|
|
110
|
+
return
|
|
111
|
+
# A one-line member shares its parent's line; listing that line twice adds nothing.
|
|
112
|
+
if item.span.start_line not in rows:
|
|
113
|
+
rows.add(item.span.start_line)
|
|
114
|
+
out.append(f"{indent}- L{item.span.start_line + 1} {_signature(item, lines)}")
|
|
115
|
+
doc = _doc(item, lines, language)
|
|
116
|
+
if doc:
|
|
117
|
+
out.append(f"{indent} doc: {doc}")
|
|
118
|
+
_symbol_lines(item.children, lines, language, depth + 1, out, rows)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def outline(path: str, data: bytes) -> str | None:
|
|
122
|
+
"""Return a text outline of one source file, or None when the suffix is unsupported or the file does not parse."""
|
|
123
|
+
language = language_for(path)
|
|
124
|
+
if language is None:
|
|
125
|
+
return None
|
|
126
|
+
try:
|
|
127
|
+
source = data.decode("utf-8")
|
|
128
|
+
except UnicodeDecodeError:
|
|
129
|
+
return None
|
|
130
|
+
_load(language)
|
|
131
|
+
try:
|
|
132
|
+
result = process(source, ProcessConfig(language=language, structure=True, imports=True, exports=False))
|
|
133
|
+
except Exception:
|
|
134
|
+
return None
|
|
135
|
+
lines = source.split("\n")
|
|
136
|
+
imports = _imports(result, language, lines)
|
|
137
|
+
symbols: list[str] = []
|
|
138
|
+
_symbol_lines(result.structure, lines, language, 0, symbols, set())
|
|
139
|
+
if not symbols and not imports:
|
|
140
|
+
return None
|
|
141
|
+
body = [f"File: {path}", f"Language: {language}"]
|
|
142
|
+
if imports:
|
|
143
|
+
body += ["Imports:"] + [f"- {item}" for item in imports]
|
|
144
|
+
if symbols:
|
|
145
|
+
body += ["Symbols:"] + symbols
|
|
146
|
+
return "\n".join(body)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch RSS and OpenAlex query results into markdown under feeds.out_dir for the indexer."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.parse
|
|
11
|
+
import urllib.request
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import index_config
|
|
16
|
+
|
|
17
|
+
USER_AGENT = "openmem-feed-sync/1.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def slugify(name: str) -> str:
|
|
21
|
+
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "feed"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fetch_bytes(url: str, timeout: int = 60) -> bytes:
|
|
25
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
26
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
27
|
+
return resp.read()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_markdown(out_dir: Path, bank_slug: str, title: str, body: str, source_url: str) -> Path:
|
|
31
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
digest = hashlib.sha256(source_url.encode()).hexdigest()[:12]
|
|
33
|
+
safe = slugify(title)[:60] or digest
|
|
34
|
+
path = out_dir / f"{bank_slug}--{safe}--{digest}.md"
|
|
35
|
+
text = f"---\nsource_url: {source_url}\nfeed_bank: feed::{bank_slug}\n---\n\n# {title}\n\n{body.strip()}\n"
|
|
36
|
+
path.write_text(text, encoding="utf-8")
|
|
37
|
+
return path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sync_rss(url: str, out_dir: Path, slug: str | None = None) -> int:
|
|
41
|
+
bank_slug = slug or slugify(urllib.parse.urlparse(url).path or "rss")
|
|
42
|
+
raw = fetch_bytes(url)
|
|
43
|
+
root = ET.fromstring(raw)
|
|
44
|
+
channel = root.find("channel") or root
|
|
45
|
+
items = channel.findall("item") if channel is not None else root.findall(".//item")
|
|
46
|
+
count = 0
|
|
47
|
+
for item in items[:50]:
|
|
48
|
+
title = (item.findtext("title") or "untitled").strip()
|
|
49
|
+
link = (item.findtext("link") or "").strip()
|
|
50
|
+
desc = (item.findtext("description") or item.findtext("{http://www.w3.org/2005/Atom}summary") or "").strip()
|
|
51
|
+
desc = re.sub(r"<[^>]+>", " ", desc)
|
|
52
|
+
if not link:
|
|
53
|
+
continue
|
|
54
|
+
write_markdown(out_dir, bank_slug, title, desc or title, link)
|
|
55
|
+
count += 1
|
|
56
|
+
return count
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def sync_openalex(query_url: str, out_dir: Path, slug: str | None = None) -> int:
|
|
60
|
+
bank_slug = slug or "openalex"
|
|
61
|
+
raw = fetch_bytes(query_url)
|
|
62
|
+
data = json.loads(raw.decode())
|
|
63
|
+
results = data.get("results") or []
|
|
64
|
+
count = 0
|
|
65
|
+
for work in results[:40]:
|
|
66
|
+
title = work.get("title") or work.get("display_name") or "work"
|
|
67
|
+
wid = work.get("id") or ""
|
|
68
|
+
abstract = work.get("abstract_inverted_index")
|
|
69
|
+
body = title
|
|
70
|
+
if isinstance(abstract, dict):
|
|
71
|
+
words = sorted(abstract.items(), key=lambda kv: kv[1][0] if kv[1] else 0)
|
|
72
|
+
body = " ".join(w for w, _ in words)
|
|
73
|
+
doi = (work.get("doi") or wid) or title
|
|
74
|
+
write_markdown(out_dir, bank_slug, title, body, str(doi))
|
|
75
|
+
count += 1
|
|
76
|
+
return count
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run(config: index_config.Config | None = None) -> int:
|
|
80
|
+
config = config or index_config.load()
|
|
81
|
+
feeds = getattr(config, "feeds_out_dir", None)
|
|
82
|
+
if feeds is None:
|
|
83
|
+
print("feeds: nothing configured (feeds.rss / feeds.openalex_queries empty)", file=sys.stderr)
|
|
84
|
+
return 0
|
|
85
|
+
out_dir = feeds
|
|
86
|
+
total = 0
|
|
87
|
+
for entry in config.feeds_rss:
|
|
88
|
+
url = entry if isinstance(entry, str) else entry.get("url", "")
|
|
89
|
+
slug = None if isinstance(entry, str) else entry.get("slug")
|
|
90
|
+
if not url:
|
|
91
|
+
continue
|
|
92
|
+
n = sync_rss(url, out_dir, slug)
|
|
93
|
+
print(f"rss {url}: {n} items -> {out_dir}")
|
|
94
|
+
total += n
|
|
95
|
+
for entry in config.feeds_openalex_queries:
|
|
96
|
+
url = entry if isinstance(entry, str) else entry.get("url", "")
|
|
97
|
+
slug = None if isinstance(entry, str) else entry.get("slug")
|
|
98
|
+
if not url:
|
|
99
|
+
continue
|
|
100
|
+
n = sync_openalex(url, out_dir, slug)
|
|
101
|
+
print(f"openalex {url}: {n} works -> {out_dir}")
|
|
102
|
+
total += n
|
|
103
|
+
return 0 if total >= 0 else 1
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
raise SystemExit(run())
|