Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ Tags, memories, project instructions, conversation history—anything Loominary

This is still taking shape. While the architecture supports it, the integration surface is still in its early stages. If you're interested in building on top of this—or have ideas about how to bridge Loominary with other AI clients—[open an issue](https://github.com/Laumss/Loominary/issues) and let's figure it out together.

Sprint 2 adds the first concrete version of that surface:

* a versioned local archive contract for conversations, branches, tags, favorites, and captured context
* a small local HTTP service with MCP-style tool calls for listing, searching, and fetching archive data
* consumer docs and example query flows for other local AI tools

See [docs/local-archive-contract.md](docs/local-archive-contract.md) and [docs/local-service-consumer-guide.md](docs/local-service-consumer-guide.md).

-----

## Privacy
Expand All @@ -98,4 +106,4 @@ Loominary is a ground-up rewrite of the original Lyra Exporter. The old codebase

## Contributing

A contributing guide and development roadmap are on the way. In the meantime, if you have any idea, please [open an discussion](https://github.com/Laumss/Loominary/discussions).
A contributing guide and development roadmap are on the way. In the meantime, if you have any idea, please [open an discussion](https://github.com/Laumss/Loominary/discussions).
212 changes: 212 additions & 0 deletions docs/local-archive-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Loominary Local Archive Contract v1

This sprint adds the first stable local archive surface for Loominary. The goal is to keep the protocol small, provider-agnostic, and durable enough for other local AI tools to consume directly.

## Design goals

- Local-first source of truth.
- Provider-agnostic normalized records.
- Separate immutable-ish archive records from rebuildable indexes.
- Expose captured context beside the conversation it belongs to.
- Keep auth simple for local-only use, with an opt-in token for stricter setups.

## On-disk layout

```text
archive-root/
manifest.json
annotations.json
conversations/
<conversation-id>.json
contexts/
<conversation-id>.json
```

## Source of truth vs derived state

Source of truth:

- `manifest.json`
- `annotations.json`
- `conversations/*.json`
- `contexts/*.json`

Derived state:

- Full-text search indexes
- Conversation summary views
- Branch tree projections

Derived state can be rebuilt at service startup. Consumers should not treat indexes as canonical.

## `manifest.json`

```json
{
"schemaVersion": "loominary.archive/v1",
"archiveId": "workspace-or-user-archive",
"createdAt": "2026-05-08T00:00:00.000Z",
"updatedAt": "2026-05-08T00:00:00.000Z",
"layout": {
"conversationsDir": "conversations",
"contextsDir": "contexts",
"annotationsFile": "annotations.json"
},
"lifecycle": {
"sourceOfTruth": [
"manifest.json",
"conversations/*.json",
"contexts/*.json",
"annotations.json"
],
"derivedIndexes": [
"rebuild-in-memory"
]
},
"trust": {
"defaultBind": "127.0.0.1",
"authMode": "loopback-or-token"
}
}
```

## Conversation record

Each conversation is a self-contained normalized record:

```json
{
"schemaVersion": "loominary.conversation/v1",
"conversation": {
"id": "conv-alpha",
"title": "Sprint architecture notes",
"platform": "claude",
"provider": "anthropic",
"providerConversationId": "provider-alpha",
"createdAt": "2026-05-07T12:00:00.000Z",
"updatedAt": "2026-05-08T02:00:00.000Z",
"favorite": true
},
"branches": [
{
"id": "main",
"rootMessageId": "msg-1",
"leafMessageIds": ["msg-3", "msg-4"]
}
],
"messages": [
{
"id": "msg-1",
"parentId": null,
"branchId": "main",
"role": "user",
"createdAt": "2026-05-07T12:00:00.000Z",
"text": "Summarize the local archive shape for Loominary.",
"content": [
{ "type": "text", "text": "Summarize the local archive shape for Loominary." }
]
}
]
}
```

Normalized rules:

- `conversation.id` is Loominary-local and stable within the archive.
- `providerConversationId` is optional provider metadata and not the primary key.
- `messages[].text` is the consumer-friendly plain text projection.
- `messages[].content` preserves extensible typed blocks.
- `branches[]` describes branch membership without exposing provider-specific tree formats.

## Context record

Context lives beside the conversation, not mixed into message bodies:

```json
{
"schemaVersion": "loominary.context/v1",
"conversationId": "conv-alpha",
"project": {
"id": "project-1",
"name": "Loominary Sprint 2",
"description": "First local integration surface.",
"instructions": "Keep outputs provider-agnostic and durable.",
"knowledgeFiles": [
{
"id": "kf-1",
"name": "README.md",
"summary": "Product promise and archive framing."
}
]
},
"memories": {
"global": [],
"project": [],
"saved": []
}
}
```

This covers the captured context called out in the README:

- project descriptions and instructions
- project memories
- saved memories
- knowledge-file metadata

## Annotations record

Tags and favorites are normalized into a single annotations file:

```json
{
"schemaVersion": "loominary.annotations/v1",
"favorites": ["conv-alpha"],
"tags": [
{
"tag": "important",
"conversationId": "conv-alpha",
"messageId": "msg-2",
"createdAt": "2026-05-08T00:00:00.000Z",
"source": "loominary"
}
]
}
```

Rules:

- Favorites are conversation-level only.
- Tags can target a whole conversation or a specific message.
- Message tags stay stable via `messageId`, not array index.

## Trust and auth model

Default trust model:

- Bind only to `127.0.0.1`.
- Treat loopback traffic as trusted when no token is configured.
- If `LOOMINARY_LOCAL_TOKEN` is set, require `Authorization: Bearer <token>` even on loopback.

Non-goals for v1:

- multi-user auth
- remote exposure
- browser-session identity forwarding

## Storage and index lifecycle

- Archive JSON files are the durable layer.
- Search and branch projections are rebuilt when the service starts.
- If a conversation or context file changes, restarting the service is enough to pick it up.
- Future sprint work can add file watching or persisted indexes without changing the record contract.

## Mapping from current Loominary state

This v1 contract is designed to absorb current product concepts cleanly:

- browser `localStorage` favorites map -> `annotations.favorites`
- browser mark/tag state -> `annotations.tags`
- normalized parser output `chat_history` -> `messages`
- detected branches -> `branches`
- export-time project/memory payloads -> `contexts/<conversation-id>.json`
125 changes: 125 additions & 0 deletions docs/local-service-consumer-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Loominary Local Service Consumer Guide

## Start the service

```bash
node server/loominary-local-service.mjs --archive ./tests/fixtures/archive-v1 --port 3788
```

Optional auth:

```bash
LOOMINARY_LOCAL_TOKEN=dev-token node server/loominary-local-service.mjs --archive ./tests/fixtures/archive-v1
```

## HTTP surface

Endpoints:

- `GET /health`
- `GET /v1/conversations`
- `GET /v1/search?q=<query>`
- `GET /v1/conversations/:conversationId`
- `GET /v1/conversations/:conversationId/tree`
- `GET /v1/conversations/:conversationId/context`
- `GET /v1/tags`
- `GET /v1/favorites`
- `POST /mcp`

## Example HTTP query flows

List recent conversations:

```bash
curl http://127.0.0.1:3788/v1/conversations
```

Search across titles, messages, tags, and context:

```bash
curl "http://127.0.0.1:3788/v1/search?q=provider-agnostic"
```

Fetch a full conversation and then its branch tree:

```bash
curl http://127.0.0.1:3788/v1/conversations/conv-alpha
curl http://127.0.0.1:3788/v1/conversations/conv-alpha/tree
```

Fetch project and memory context:

```bash
curl http://127.0.0.1:3788/v1/conversations/conv-alpha/context
```

Filter to favorites or a tag:

```bash
curl "http://127.0.0.1:3788/v1/conversations?favoriteOnly=true"
curl "http://127.0.0.1:3788/v1/conversations?tag=research"
```

## Example MCP flows

List available tools:

```bash
curl -X POST http://127.0.0.1:3788/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'
```

Search conversations via MCP:

```bash
curl -X POST http://127.0.0.1:3788/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_conversations",
"arguments": {
"query": "project memories"
}
}
}'
```

Fetch attached context via MCP:

```bash
curl -X POST http://127.0.0.1:3788/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_context",
"arguments": {
"conversationId": "conv-alpha"
}
}
}'
```

## Consumer expectations

- Treat conversation IDs as Loominary-local IDs.
- Prefer `messages[].text` for simple prompt/context assembly.
- Use `messages[].content` when you need typed rendering later.
- Pull context separately so tools can choose when to include project instructions or saved memories.
- Do not depend on provider-specific payloads being present.

## Current limitations

- Archive creation is not yet automated from the browser app.
- Search is in-memory and rebuilt at startup.
- The MCP surface is tool-call oriented and intentionally narrow for v1.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"start": "cross-env HOST=0.0.0.0 PORT=3789 PUBLIC_URL=/lyra-exporter react-scripts start",
"start:local": "cross-env PORT=3789 react-scripts start",
"start:network": "cross-env HOST=0.0.0.0 PORT=3789 react-scripts start",
"local-service": "node server/loominary-local-service.mjs",
"test:local-service": "node --test tests/*.test.mjs",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 react-scripts build",
"postbuild": "node -e \"const fs=require('fs');fs.mkdirSync('build/welcome',{recursive:true});let h=fs.readFileSync('build/index.html','utf8');h=h.replace(/\\.\\//g,'../');fs.writeFileSync('build/welcome/index.html',h)\"",
"build:extension": "cross-env NODE_OPTIONS=--max-old-space-size=4096 react-scripts build",
Expand Down
Loading
Loading