@genspark/opencode-conversation-archiver 0.1.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/README.md +164 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +41 -0
- package/dist/core.d.ts +77 -0
- package/dist/core.js +833 -0
- package/dist/push-worker.d.ts +1 -0
- package/dist/push-worker.js +12 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +160 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# OpenCode conversation archiver
|
|
2
|
+
|
|
3
|
+
An OpenCode server plugin that archives every top-level conversation after each
|
|
4
|
+
completed turn. It keeps one Markdown file per session in a git repository and
|
|
5
|
+
records only user-visible text:
|
|
6
|
+
|
|
7
|
+
- user text parts;
|
|
8
|
+
- assistant text parts;
|
|
9
|
+
- context-compaction boundaries.
|
|
10
|
+
|
|
11
|
+
Reasoning, tool calls, tool outputs, file URLs, synthetic text, ignored text,
|
|
12
|
+
and child/subagent sessions are excluded.
|
|
13
|
+
|
|
14
|
+
## Install From This Repository
|
|
15
|
+
|
|
16
|
+
Build the package:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cd toolkits/opencode-conversation-archiver
|
|
20
|
+
npm install
|
|
21
|
+
npm run build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Register the built server entry in `opencode.json`:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"$schema": "https://opencode.ai/config.json",
|
|
29
|
+
"plugin": [
|
|
30
|
+
"./toolkits/opencode-conversation-archiver/dist/server.js"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The path is relative to the config file. Quit and restart OpenCode after adding
|
|
36
|
+
or updating a plugin because configuration is loaded only at startup.
|
|
37
|
+
|
|
38
|
+
For package distribution, the package exposes OpenCode's native `./server`
|
|
39
|
+
entry and declares compatibility with OpenCode `1.17.18` through `<2`.
|
|
40
|
+
|
|
41
|
+
## Default Behavior
|
|
42
|
+
|
|
43
|
+
- Archive repo: `~/opencode-conversations`
|
|
44
|
+
- Runtime state: `~/.config/opencode/conversation-archiver/`
|
|
45
|
+
- Mode: `auto`
|
|
46
|
+
- Trigger: canonical `session.status` event with `status.type === "idle"`
|
|
47
|
+
- Auto mode: write, commit, and launch a detached push after every changed turn
|
|
48
|
+
- Manual mode: write locally without commit or push
|
|
49
|
+
- No remote: commits remain local and push failure never interrupts OpenCode
|
|
50
|
+
|
|
51
|
+
The event hook owns a per-session queue because OpenCode intentionally does not
|
|
52
|
+
await plugin event callbacks. `dispose` drains pending snapshots during a clean
|
|
53
|
+
shutdown. Git and tmux commands use asynchronous child processes so archive
|
|
54
|
+
work does not block OpenCode's event loop.
|
|
55
|
+
|
|
56
|
+
## Management Tools
|
|
57
|
+
|
|
58
|
+
The plugin exposes these native tools:
|
|
59
|
+
|
|
60
|
+
| Tool | Purpose |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `conversation_archive_status` | Mode, repo, redacted remote, commits, pending changes |
|
|
63
|
+
| `conversation_archive_doctor` | Read-only dependency, repo, remote-auth, and activity diagnosis |
|
|
64
|
+
| `conversation_archive_current` | Archive the current live session immediately |
|
|
65
|
+
| `conversation_archive_backfill` | Archive all top-level sessions visible in the current OpenCode project |
|
|
66
|
+
|
|
67
|
+
Users can ask OpenCode to invoke these tools naturally, for example: "diagnose
|
|
68
|
+
my conversation archive". The two write tools request explicit permission.
|
|
69
|
+
Credential-bearing and repository-management operations are CLI-only so their
|
|
70
|
+
arguments never enter an LLM-visible conversation or tool call.
|
|
71
|
+
|
|
72
|
+
OpenCode `1.17.18` does not expose a direct, no-LLM server slash-command handler.
|
|
73
|
+
Markdown commands always enter the prompt pipeline and `command.execute.before`
|
|
74
|
+
cannot short-circuit it. For deterministic automation, use the bundled CLI:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
opencode-conversation-archiver status
|
|
78
|
+
opencode-conversation-archiver doctor
|
|
79
|
+
opencode-conversation-archiver manual
|
|
80
|
+
opencode-conversation-archiver auto
|
|
81
|
+
opencode-conversation-archiver upload
|
|
82
|
+
opencode-conversation-archiver repo ~/Documents/opencode-conversations
|
|
83
|
+
opencode-conversation-archiver connect '<sb-connect-link>'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`backfill` and `current` remain live tools because they require the injected
|
|
87
|
+
OpenCode SDK and current session identity.
|
|
88
|
+
|
|
89
|
+
## Second Brain
|
|
90
|
+
|
|
91
|
+
`conversation_archive_connect` and the CLI `connect` support:
|
|
92
|
+
|
|
93
|
+
1. A one-time `.../sb-connect/<code>` URL.
|
|
94
|
+
2. No arguments, using `GSK_API_KEY` or the credential created by `gsk login`.
|
|
95
|
+
3. An explicit HTTP(S) git remote and token.
|
|
96
|
+
|
|
97
|
+
The git credential is stored at
|
|
98
|
+
`~/.config/opencode/conversation-archiver/git-credentials` with mode `0600`,
|
|
99
|
+
outside the archive repository. The remote URL shown by status, doctor, and logs
|
|
100
|
+
is credential-redacted. Connected archives are confined to the `opencode/`
|
|
101
|
+
subdirectory by default with sparse checkout.
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
Runtime configuration:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"mode": "auto",
|
|
110
|
+
"repo": "/home/you/opencode-conversations",
|
|
111
|
+
"subdir": "opencode"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Environment overrides:
|
|
116
|
+
|
|
117
|
+
- `OPENCODE_ARCHIVE_MODE`
|
|
118
|
+
- `OPENCODE_ARCHIVE_REPO`
|
|
119
|
+
- `OPENCODE_ARCHIVE_NO_NOTIFY`
|
|
120
|
+
|
|
121
|
+
For isolated tests or embedded installations, pass `appDir` as plugin options:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"plugin": [
|
|
126
|
+
[
|
|
127
|
+
"@genspark/opencode-conversation-archiver",
|
|
128
|
+
{ "appDir": "/custom/runtime-state" }
|
|
129
|
+
]
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Preservation Guarantees
|
|
135
|
+
|
|
136
|
+
- Message and compaction part IDs make overlapping/repeated idle events idempotent.
|
|
137
|
+
- Cumulative text for an existing message ID can grow without duplicating a turn.
|
|
138
|
+
- Session state is append-only and outside the git repo.
|
|
139
|
+
- Title changes rename the archive while preserving git history where possible.
|
|
140
|
+
- A collision index prevents one session from overwriting another.
|
|
141
|
+
- A never-shrink guard diverts to a suffixed file if disk content is richer than
|
|
142
|
+
local state, preserving both copies instead of dropping turns.
|
|
143
|
+
- Archive and push operations use cross-process lock directories.
|
|
144
|
+
- Remote pushes serialize, pull with rebase, and abort a failed in-progress rebase.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm test
|
|
150
|
+
npm run typecheck
|
|
151
|
+
npm run build
|
|
152
|
+
npm pack --dry-run
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Local End-to-End Verification
|
|
156
|
+
|
|
157
|
+
The release was verified against a real local OpenCode `1.17.18` process, not
|
|
158
|
+
only mocked SDK tests. An isolated `opencode run` session using Claude Haiku
|
|
159
|
+
completed with `ARCHIVER_E2E_OK`; the plugin then created a titled Markdown
|
|
160
|
+
archive containing one user and one assistant turn and committed it to the
|
|
161
|
+
isolated archive git repository. The temporary plugin config and archive paths
|
|
162
|
+
were supplied through `OPENCODE_CONFIG_CONTENT`, `appDir`, and
|
|
163
|
+
`OPENCODE_ARCHIVE_REPO`, so the normal OpenCode configuration and archive were
|
|
164
|
+
not modified.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { connect, doctor, setMode, setRepo, status, upload } from './core.js';
|
|
3
|
+
const [command, ...args] = process.argv.slice(2);
|
|
4
|
+
function requireSuccess(result) {
|
|
5
|
+
if (/failed|failure|not initialized|requires/i.test(result))
|
|
6
|
+
throw new Error(result);
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
async function main() {
|
|
10
|
+
switch (command) {
|
|
11
|
+
case 'auto':
|
|
12
|
+
return setMode('auto');
|
|
13
|
+
case 'manual':
|
|
14
|
+
return setMode('manual');
|
|
15
|
+
case 'upload':
|
|
16
|
+
return requireSuccess(await upload());
|
|
17
|
+
case 'repo':
|
|
18
|
+
if (!args[0])
|
|
19
|
+
throw new Error('usage: opencode-conversation-archiver repo <absolute path or ~/path>');
|
|
20
|
+
return setRepo(args[0]);
|
|
21
|
+
case 'status':
|
|
22
|
+
return status();
|
|
23
|
+
case 'doctor':
|
|
24
|
+
return doctor();
|
|
25
|
+
case 'connect':
|
|
26
|
+
return requireSuccess(await connect(args[0] || '', args[1] || '', args[2] || 'opencode'));
|
|
27
|
+
default:
|
|
28
|
+
throw new Error([
|
|
29
|
+
'usage: opencode-conversation-archiver <command>',
|
|
30
|
+
'commands: auto, manual, upload, repo <path>, status, doctor, connect [link|remote] [token] [subdir]',
|
|
31
|
+
'backfill and current-session archive are exposed as OpenCode tools because they require the live OpenCode SDK.',
|
|
32
|
+
].join('\n'));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
console.log(await main());
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Message, Part, Session } from '@opencode-ai/sdk';
|
|
2
|
+
export type Mode = 'auto' | 'manual';
|
|
3
|
+
export type BlockRole = 'user' | 'assistant' | 'compact';
|
|
4
|
+
export type Block = {
|
|
5
|
+
key: string;
|
|
6
|
+
role: BlockRole;
|
|
7
|
+
text: string;
|
|
8
|
+
};
|
|
9
|
+
export type Conversation = {
|
|
10
|
+
session: Session;
|
|
11
|
+
messages: Array<{
|
|
12
|
+
info: Message;
|
|
13
|
+
parts: Part[];
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
export type Config = {
|
|
17
|
+
mode?: Mode;
|
|
18
|
+
repo?: string;
|
|
19
|
+
subdir?: string;
|
|
20
|
+
};
|
|
21
|
+
type State = {
|
|
22
|
+
title: string;
|
|
23
|
+
file: string | null;
|
|
24
|
+
start: number;
|
|
25
|
+
blocks: Block[];
|
|
26
|
+
machine: {
|
|
27
|
+
hostname?: string;
|
|
28
|
+
ip?: string;
|
|
29
|
+
tmux?: string;
|
|
30
|
+
};
|
|
31
|
+
diverted: boolean;
|
|
32
|
+
};
|
|
33
|
+
export type Paths = {
|
|
34
|
+
app: string;
|
|
35
|
+
config: string;
|
|
36
|
+
state: string;
|
|
37
|
+
index: string;
|
|
38
|
+
log: string;
|
|
39
|
+
pushLog: string;
|
|
40
|
+
credentials: string;
|
|
41
|
+
};
|
|
42
|
+
export declare function defaultPaths(home?: string): Paths;
|
|
43
|
+
export declare function expandHome(path: string, home?: string): string;
|
|
44
|
+
export declare function maskUrl(text: string): string;
|
|
45
|
+
export declare function slugify(value: string): string;
|
|
46
|
+
export declare function extractBlocks(messages: Conversation['messages']): Block[];
|
|
47
|
+
export declare function renderMarkdown(state: State, sessionID: string): string;
|
|
48
|
+
export declare function bodyCovers(oldBody: string, newBody: string): boolean;
|
|
49
|
+
export declare function loadConfig(paths?: Paths): Promise<Config>;
|
|
50
|
+
export declare function saveConfig(config: Config, paths?: Paths): Promise<void>;
|
|
51
|
+
export declare function effectiveRepo(config: Config, home?: string): string;
|
|
52
|
+
export declare function effectiveMode(config: Config): Mode;
|
|
53
|
+
export type GitResult = {
|
|
54
|
+
status: number;
|
|
55
|
+
stdout: string;
|
|
56
|
+
stderr: string;
|
|
57
|
+
};
|
|
58
|
+
export declare function run(repo: string, command: string, args: string[], timeout?: number): Promise<GitResult>;
|
|
59
|
+
export declare function git(repo: string, args: string[], timeout?: number): Promise<GitResult>;
|
|
60
|
+
export declare function push(repo: string, paths?: Paths): Promise<GitResult>;
|
|
61
|
+
export declare function archiveConversation(conversation: Conversation, paths?: Paths, commit?: boolean): Promise<{
|
|
62
|
+
file: string;
|
|
63
|
+
turns: number;
|
|
64
|
+
}>;
|
|
65
|
+
export declare function commitPending(paths?: Paths, message?: string): Promise<{
|
|
66
|
+
repo: string;
|
|
67
|
+
committed: boolean;
|
|
68
|
+
error?: string;
|
|
69
|
+
}>;
|
|
70
|
+
export declare function upload(paths?: Paths): Promise<string>;
|
|
71
|
+
export declare function setMode(mode: Mode, paths?: Paths): Promise<string>;
|
|
72
|
+
export declare function setRepo(path: string, paths?: Paths): Promise<string>;
|
|
73
|
+
export declare function status(paths?: Paths): Promise<string>;
|
|
74
|
+
export declare function doctor(paths?: Paths): Promise<string>;
|
|
75
|
+
export declare function connect(remoteUrl: string, token: string, subdir?: string, paths?: Paths): Promise<string>;
|
|
76
|
+
export declare function emitNotification(sessionID: string, title: string, turns: number): Promise<void>;
|
|
77
|
+
export {};
|