@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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 {};