Skip to content
Merged

Dev #57

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@ Maintainers are not obligated to:
This project is licensed under [AGPL-3.0](LICENSE).
Commercial use violating the licence will be subject to legal action.

Enforcement of this code of conduct is at the maintainers' sole discretion; final interpretation rests with [@TrueNine](https://github.com/TrueNine).
Enforcement of this code of conduct is at the maintainers' sole discretion; final interpretation rests with [@TrueNine](https://github.com/TrueNine).
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Rats 🐀 are like this: even our own brains, even our memories, are things we haul around while running through this fucked-up world!!!

I am a rat. No resources will ever be proactively provided to me.\
I am a rat. No resources will ever be proactively provided to me.
So as a rat, I eat whatever I can reach: maggots in the sewer, leftovers in the slop bucket, and in extreme cases even my own kind—this is the survival mode in a world where resource allocation is brutally unfair.

`memory-sync` is the same kind of **tool-rat**:
Expand All @@ -11,7 +11,7 @@ So as a rat, I eat whatever I can reach: maggots in the sewer, leftovers in the
- Does not rely on privileged interfaces of any single IDE / CLI
- Treats every readable config, prompt, and memory file as "edible matter" to be carried, dismantled, and recombined

In this ecosystem, giants monopolise the resources, and developers are thrown into the corner like rats.\
In this ecosystem, giants monopolise the resources, and developers are thrown into the corner like rats.
`memory-sync` accepts this cruel reality, does not fantasise about fairness, and focuses on one thing only: **to chew up every fragment of resource you already have, and convert it into portable "memory" that can flow between any AI tool.**

![rat](/.attachments/rat.svg)
Expand Down Expand Up @@ -76,38 +76,38 @@ To use `memory-sync` you need:

---

- You are writing code in a forgotten sewer.\
- You are writing code in a forgotten sewer.
No one will proactively feed you, not even a tiny free quota, not even a half-decent document.
- As a rat, you can barely get your hands on anything good:\
- As a rat, you can barely get your hands on anything good:
scurrying between free tiers, trial credits, education discounts, and random third-party scripts.
- What can you do?\
- What can you do?
Keep darting between IDEs, CLIs, browser extensions, and cloud Agents, copying and pasting the same memory a hundred times.
- You leech API offers from vendors day after day:\
- You leech API offers from vendors day after day:
today one platform runs a discount so you top up a little; tomorrow another launches a promo so you rush to scrape it.
- Once they have harvested the telemetry, user profiles, and usage patterns they want,\
- Once they have harvested the telemetry, user profiles, and usage patterns they want,
they can kick you—this stinking rat—away at any moment: price hikes, rate limits, account bans, and you have no channel to complain.

If you are barely surviving in this environment, `memory-sync` is built for you:\
If you are barely surviving in this environment, `memory-sync` is built for you:
carry fewer bricks, copy prompts fewer times—at least on the "memory" front, you are no longer completely on the passive receiving end.

## Who is NOT welcome

- Your income is already fucking high.\
- Your income is already fucking high.
Stable salary, project revenue share, budget to sign official APIs yearly.
- And yet you still come down here,\
- And yet you still come down here,
competing with us filthy sewer rats for the scraps in the slop bucket.
- If you can afford APIs and enterprise plans, go pay for them.\
- If you can afford APIs and enterprise plans, go pay for them.
Do things that actually create value—pay properly, give proper feedback, nudge the ecosystem slightly in the right direction.
- Instead of coming back down\
- Instead of coming back down
to strip away the tiny gap left for marginalised developers, squeezing out the last crumbs with us rats.
- You are a freeloader.\
- You are a freeloader.
Everything must be pre-chewed and spoon-fed; you won't even touch a terminal.
- You love the grind culture.\
- You love the grind culture.
Treating "hustle" as virtue, "996" as glory, stepping on peers as a promotion strategy.
- You leave no room for others.\
- You leave no room for others.
Not about whether you share—it's about actively stomping on people, competing maliciously, sustaining your position by suppressing peers, using others' survival space as your stepping stone.

In other words:\
In other words:
**this is not a tool for optimising capital costs, but a small counterattack prepared for the "rats with no choice" in a world of extreme resource inequality.**

## Created by
Expand Down
8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
Only the latest release receives security fixes. No backport patches for older versions.

| Version | Supported |
|---------|-----------|
| Latest | ✅ |
| Older | ❌ |
| ------- | --------- |
| Latest | ✅ |
| Older | ❌ |

## Reporting a Vulnerability

Expand Down Expand Up @@ -58,4 +58,4 @@ The following are **out of scope**:

## License

This project is licensed under [AGPL-3.0](LICENSE). Unauthorised commercial use in violation of the licence will be pursued legally.
This project is licensed under [AGPL-3.0](LICENSE). Unauthorised commercial use in violation of the licence will be pursued legally.
2 changes: 1 addition & 1 deletion cli/eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const config = eslint10({
strictTypescriptEslint: true,
tsconfigPath: resolve(configDir, 'tsconfig.eslint.json'),
parserOptions: {
allowDefaultProject: true
allowDefaultProject: ['*.config.ts']
}
},
ignores: [
Expand Down
2 changes: 1 addition & 1 deletion cli/npm/darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@truenine/memory-sync-cli-darwin-arm64",
"version": "2026.10224.10619",
"version": "2026.10302.10037",
"os": [
"darwin"
],
Expand Down
2 changes: 1 addition & 1 deletion cli/npm/darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@truenine/memory-sync-cli-darwin-x64",
"version": "2026.10224.10619",
"version": "2026.10302.10037",
"os": [
"darwin"
],
Expand Down
2 changes: 1 addition & 1 deletion cli/npm/linux-arm64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@truenine/memory-sync-cli-linux-arm64-gnu",
"version": "2026.10224.10619",
"version": "2026.10302.10037",
"os": [
"linux"
],
Expand Down
2 changes: 1 addition & 1 deletion cli/npm/linux-x64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@truenine/memory-sync-cli-linux-x64-gnu",
"version": "2026.10224.10619",
"version": "2026.10302.10037",
"os": [
"linux"
],
Expand Down
2 changes: 1 addition & 1 deletion cli/npm/win32-x64-msvc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@truenine/memory-sync-cli-win32-x64-msvc",
"version": "2026.10224.10619",
"version": "2026.10302.10037",
"os": [
"win32"
],
Expand Down
47 changes: 3 additions & 44 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@truenine/memory-sync-cli",
"type": "module",
"version": "2026.10224.10619",
"version": "2026.10302.10037",
"description": "TrueNine Memory Synchronization CLI",
"author": "TrueNine",
"license": "AGPL-3.0-only",
Expand Down Expand Up @@ -39,11 +39,12 @@
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"build": "run-s build:deps check bundle",
"build": "run-s build:deps check bundle generate:schema",
"build:napi": "tsx ../scripts/copy-napi.ts",
"build:deps": "pnpm exec turbo run build --filter=...@truenine/memory-sync-cli --filter=!@truenine/memory-sync-cli",
"bundle": "pnpm exec tsdown",
"check": "run-p typecheck lint",
"generate:schema": "tsx scripts/generate-schema.ts",
"lint": "eslint --cache .",
"prepublishOnly": "run-s build",
"test": "run-s build:deps test:run",
Expand Down Expand Up @@ -73,51 +74,9 @@
"@truenine/memory-sync-cli-win32-x64-msvc": "workspace:*"
},
"devDependencies": {
"@truenine/desk-paths": "workspace:*",
"@truenine/init-bundle": "workspace:*",
"@truenine/logger": "workspace:*",
"@truenine/md-compiler": "workspace:*",
"@truenine/plugin-agentskills-compact": "workspace:*",
"@truenine/plugin-agentsmd": "workspace:*",
"@truenine/plugin-antigravity": "workspace:*",
"@truenine/plugin-claude-code-cli": "workspace:*",
"@truenine/plugin-cursor": "workspace:*",
"@truenine/plugin-droid-cli": "workspace:*",
"@truenine/plugin-editorconfig": "workspace:*",
"@truenine/plugin-gemini-cli": "workspace:*",
"@truenine/plugin-git-exclude": "workspace:*",
"@truenine/plugin-input-agentskills": "workspace:*",
"@truenine/plugin-input-editorconfig": "workspace:*",
"@truenine/plugin-input-fast-command": "workspace:*",
"@truenine/plugin-input-git-exclude": "workspace:*",
"@truenine/plugin-input-gitignore": "workspace:*",
"@truenine/plugin-input-global-memory": "workspace:*",
"@truenine/plugin-input-jetbrains-config": "workspace:*",
"@truenine/plugin-input-md-cleanup-effect": "workspace:*",
"@truenine/plugin-input-orphan-cleanup-effect": "workspace:*",
"@truenine/plugin-input-project-prompt": "workspace:*",
"@truenine/plugin-input-readme": "workspace:*",
"@truenine/plugin-input-rule": "workspace:*",
"@truenine/plugin-input-shadow-project": "workspace:*",
"@truenine/plugin-input-shared": "workspace:*",
"@truenine/plugin-input-shared-ignore": "workspace:*",
"@truenine/plugin-input-skill-sync-effect": "workspace:*",
"@truenine/plugin-input-subagent": "workspace:*",
"@truenine/plugin-input-vscode-config": "workspace:*",
"@truenine/plugin-input-workspace": "workspace:*",
"@truenine/plugin-jetbrains-ai-codex": "workspace:*",
"@truenine/plugin-jetbrains-codestyle": "workspace:*",
"@truenine/plugin-kiro-ide": "workspace:*",
"@truenine/plugin-openai-codex-cli": "workspace:*",
"@truenine/plugin-opencode-cli": "workspace:*",
"@truenine/plugin-output-shared": "workspace:*",
"@truenine/plugin-qoder-ide": "workspace:*",
"@truenine/plugin-readme": "workspace:*",
"@truenine/plugin-shared": "workspace:*",
"@truenine/plugin-trae-ide": "workspace:*",
"@truenine/plugin-vscode": "workspace:*",
"@truenine/plugin-warp-ide": "workspace:*",
"@truenine/plugin-windsurf": "workspace:*",
"@types/fs-extra": "catalog:",
"@types/picomatch": "catalog:",
"@vitest/coverage-v8": "catalog:",
Expand Down
5 changes: 5 additions & 0 deletions cli/scripts/generate-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {writeFileSync} from 'node:fs'
import {TNMSC_JSON_SCHEMA} from '../src/schema.ts'

writeFileSync('./dist/tnmsc.schema.json', `${JSON.stringify(TNMSC_JSON_SCHEMA, null, 2)}\n`, 'utf8')
console.log('Schema generated successfully!')
136 changes: 1 addition & 135 deletions cli/src/ConfigLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@ import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
import {ConfigLoader, DEFAULT_CONFIG_FILE_NAME, DEFAULT_GLOBAL_CONFIG_DIR, ensureConfigLink, loadUserConfig} from './ConfigLoader'
import {ConfigLoader, DEFAULT_CONFIG_FILE_NAME, DEFAULT_GLOBAL_CONFIG_DIR, loadUserConfig} from './ConfigLoader'

vi.mock('node:fs') // Mock fs module
vi.mock('node:os')
vi.mock('@truenine/desk-paths', () => ({
isSymlink: vi.fn(),
readSymlinkTarget: vi.fn(),
deletePathSync: vi.fn()
}))

describe('configLoader', () => {
const mockHomedir = '/home/testuser'
Expand Down Expand Up @@ -333,132 +328,3 @@ describe('configLoader', () => {
})
})
})

describe('ensureConfigLink', () => {
let deskPaths: typeof import('@truenine/desk-paths')

const LOCAL = '/shadow/.tnmsc.json'
const GLOBAL = '/home/testuser/.aindex/.tnmsc.json'

const logger = {
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
fatal: vi.fn()
}

beforeEach(async () => {
deskPaths = await import('@truenine/desk-paths')
vi.mocked(os.homedir).mockReturnValue('/home/testuser')
vi.mocked(fs.existsSync).mockReturnValue(false)
vi.mocked(fs.symlinkSync).mockImplementation(() => void 0)
vi.mocked(fs.copyFileSync).mockImplementation(() => void 0)
vi.mocked(deskPaths.isSymlink).mockReturnValue(false)
vi.mocked(deskPaths.readSymlinkTarget).mockReturnValue(null)
vi.mocked(deskPaths.deletePathSync).mockImplementation(() => void 0)
})

afterEach(() => vi.clearAllMocks())

it('no-op when global config does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false)

ensureConfigLink(LOCAL, GLOBAL, logger)

expect(fs.symlinkSync).not.toHaveBeenCalled()
expect(fs.copyFileSync).not.toHaveBeenCalled()
})

it('creates symlink when local file does not exist', () => {
vi.mocked(fs.existsSync).mockImplementation(p => p === GLOBAL)
vi.mocked(deskPaths.isSymlink).mockReturnValue(false)

ensureConfigLink(LOCAL, GLOBAL, logger)

expect(fs.symlinkSync).toHaveBeenCalledWith(GLOBAL, LOCAL, 'file')
})

it('no-op when local is a correct symlink pointing to global', () => {
vi.mocked(fs.existsSync).mockImplementation(p => p === GLOBAL || p === LOCAL)
vi.mocked(deskPaths.isSymlink).mockReturnValue(true)
vi.mocked(deskPaths.readSymlinkTarget).mockReturnValue(GLOBAL)

ensureConfigLink(LOCAL, GLOBAL, logger)

expect(fs.symlinkSync).not.toHaveBeenCalled()
expect(deskPaths.deletePathSync).not.toHaveBeenCalled()
})

it('deletes stale symlink and recreates when target differs', () => {
vi.mocked(fs.existsSync).mockImplementation(p => p === GLOBAL || p === LOCAL)
vi.mocked(deskPaths.isSymlink).mockReturnValue(true)
vi.mocked(deskPaths.readSymlinkTarget).mockReturnValue('/other/path/.tnmsc.json')

ensureConfigLink(LOCAL, GLOBAL, logger)

expect(deskPaths.deletePathSync).toHaveBeenCalledWith(LOCAL)
expect(fs.symlinkSync).toHaveBeenCalledWith(GLOBAL, LOCAL, 'file')
})

it('syncs regular file back to global when local content differs, then recreates symlink', () => {
vi.mocked(fs.existsSync).mockImplementation(p => p === GLOBAL || p === LOCAL)
vi.mocked(deskPaths.isSymlink).mockReturnValue(false)
vi.mocked(fs.readFileSync).mockImplementation(p => {
if (p === LOCAL) return '{"local":true}'
return '{"global":true}'
})

ensureConfigLink(LOCAL, GLOBAL, logger)

expect(fs.copyFileSync).toHaveBeenCalledWith(LOCAL, GLOBAL)
expect(deskPaths.deletePathSync).toHaveBeenCalledWith(LOCAL)
expect(fs.symlinkSync).toHaveBeenCalledWith(GLOBAL, LOCAL, 'file')
})

it('deletes regular file without sync-back when local content matches global', () => {
vi.mocked(fs.existsSync).mockImplementation(p => p === GLOBAL || p === LOCAL)
vi.mocked(deskPaths.isSymlink).mockReturnValue(false)
vi.mocked(fs.readFileSync).mockReturnValue('{"same":true}')

ensureConfigLink(LOCAL, GLOBAL, logger)

expect(fs.copyFileSync).not.toHaveBeenCalledWith(LOCAL, GLOBAL)
expect(deskPaths.deletePathSync).toHaveBeenCalledWith(LOCAL)
expect(fs.symlinkSync).toHaveBeenCalledWith(GLOBAL, LOCAL, 'file')
})

it('falls back to copy when symlink fails', () => {
vi.mocked(fs.existsSync).mockImplementation(p => p === GLOBAL)
vi.mocked(deskPaths.isSymlink).mockReturnValue(false)
vi.mocked(fs.symlinkSync).mockImplementation(() => {
throw new Error('EPERM: operation not permitted')
})

ensureConfigLink(LOCAL, GLOBAL, logger)

expect(fs.copyFileSync).toHaveBeenCalledWith(GLOBAL, LOCAL)
expect(logger.warn).toHaveBeenCalledWith(
'symlink unavailable, copied config (auto-sync disabled)',
expect.objectContaining({dest: LOCAL})
)
})

it('logs warn and does not throw when both symlink and copy fail', () => {
vi.mocked(fs.existsSync).mockImplementation(p => p === GLOBAL)
vi.mocked(deskPaths.isSymlink).mockReturnValue(false)
vi.mocked(fs.symlinkSync).mockImplementation(() => {
throw new Error('EPERM')
})
vi.mocked(fs.copyFileSync).mockImplementation(() => {
throw new Error('ENOENT')
})

expect(() => ensureConfigLink(LOCAL, GLOBAL, logger)).not.toThrow()
expect(logger.warn).toHaveBeenCalledWith(
'failed to link or copy config',
expect.objectContaining({path: LOCAL, error: 'ENOENT'})
)
})
})
Loading
Loading