@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defaultPaths, push } from './core.js';
|
|
2
|
+
const repo = process.argv[2];
|
|
3
|
+
const app = process.argv[3];
|
|
4
|
+
if (repo) {
|
|
5
|
+
const paths = defaultPaths();
|
|
6
|
+
if (app) {
|
|
7
|
+
paths.app = app;
|
|
8
|
+
paths.pushLog = `${app}/push.log`;
|
|
9
|
+
paths.log = `${app}/archive.log`;
|
|
10
|
+
}
|
|
11
|
+
await push(repo, paths);
|
|
12
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin';
|
|
2
|
+
import { archiveConversation, commitPending, defaultPaths, doctor, emitNotification, status, upload, } from './core.js';
|
|
3
|
+
function optionPaths(options) {
|
|
4
|
+
const paths = defaultPaths();
|
|
5
|
+
const appDir = typeof options?.appDir === 'string' ? options.appDir : undefined;
|
|
6
|
+
if (!appDir)
|
|
7
|
+
return paths;
|
|
8
|
+
return {
|
|
9
|
+
app: appDir,
|
|
10
|
+
config: `${appDir}/config.json`,
|
|
11
|
+
state: `${appDir}/state`,
|
|
12
|
+
index: `${appDir}/state/_index.json`,
|
|
13
|
+
log: `${appDir}/archive.log`,
|
|
14
|
+
pushLog: `${appDir}/push.log`,
|
|
15
|
+
credentials: `${appDir}/git-credentials`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function fetchConversation(input, sessionID) {
|
|
19
|
+
const [sessionResult, messagesResult] = await Promise.all([
|
|
20
|
+
input.client.session.get({ path: { id: sessionID }, throwOnError: true }),
|
|
21
|
+
input.client.session.messages({
|
|
22
|
+
path: { id: sessionID },
|
|
23
|
+
throwOnError: true,
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
if (!sessionResult.data || !messagesResult.data)
|
|
27
|
+
throw new Error(`OpenCode returned no data for session ${sessionID}`);
|
|
28
|
+
return { session: sessionResult.data, messages: messagesResult.data };
|
|
29
|
+
}
|
|
30
|
+
async function report(input, sessionID, error) {
|
|
31
|
+
await input.client.app
|
|
32
|
+
.log({
|
|
33
|
+
body: {
|
|
34
|
+
service: 'opencode-conversation-archiver',
|
|
35
|
+
level: 'error',
|
|
36
|
+
message: 'Failed to archive conversation',
|
|
37
|
+
extra: {
|
|
38
|
+
sessionID,
|
|
39
|
+
error: error instanceof Error ? error.message : String(error),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
.catch(() => undefined);
|
|
44
|
+
}
|
|
45
|
+
export const server = async (input, rawOptions) => {
|
|
46
|
+
const options = rawOptions;
|
|
47
|
+
const paths = optionPaths(options);
|
|
48
|
+
const pending = new Map();
|
|
49
|
+
const enqueue = (sessionID, notify) => {
|
|
50
|
+
const previous = pending.get(sessionID) ?? Promise.resolve();
|
|
51
|
+
const run = previous
|
|
52
|
+
.catch(() => undefined)
|
|
53
|
+
.then(async () => {
|
|
54
|
+
try {
|
|
55
|
+
const conversation = await fetchConversation(input, sessionID);
|
|
56
|
+
if (conversation.session.projectID !== input.project.id ||
|
|
57
|
+
conversation.session.parentID) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const result = await archiveConversation(conversation, paths);
|
|
61
|
+
if (notify && result.file)
|
|
62
|
+
await emitNotification(sessionID, conversation.session.title, result.turns);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
await report(input, sessionID, error);
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.finally(() => {
|
|
69
|
+
if (pending.get(sessionID) === run)
|
|
70
|
+
pending.delete(sessionID);
|
|
71
|
+
});
|
|
72
|
+
pending.set(sessionID, run);
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
event: async ({ event }) => {
|
|
76
|
+
if (event.type === 'session.status' &&
|
|
77
|
+
event.properties.status.type === 'idle') {
|
|
78
|
+
enqueue(event.properties.sessionID, true);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
dispose: async () => {
|
|
82
|
+
await Promise.allSettled(pending.values());
|
|
83
|
+
},
|
|
84
|
+
tool: {
|
|
85
|
+
conversation_archive_status: tool({
|
|
86
|
+
description: 'Show the OpenCode conversation archive mode, repository, remote, commits, and pending changes.',
|
|
87
|
+
args: {},
|
|
88
|
+
execute: async () => status(paths),
|
|
89
|
+
}),
|
|
90
|
+
conversation_archive_doctor: tool({
|
|
91
|
+
description: 'Run a read-only diagnosis of the OpenCode conversation archiver, including git remote authentication.',
|
|
92
|
+
args: {},
|
|
93
|
+
execute: async () => doctor(paths),
|
|
94
|
+
}),
|
|
95
|
+
conversation_archive_current: tool({
|
|
96
|
+
description: 'Immediately archive the current OpenCode conversation. Normally automatic at the end of every turn.',
|
|
97
|
+
args: {},
|
|
98
|
+
execute: async (_args, context) => {
|
|
99
|
+
await context.ask({
|
|
100
|
+
permission: 'conversation-archive-write',
|
|
101
|
+
patterns: ['current session'],
|
|
102
|
+
always: [],
|
|
103
|
+
metadata: {},
|
|
104
|
+
});
|
|
105
|
+
const conversation = await fetchConversation(input, context.sessionID);
|
|
106
|
+
if (conversation.session.projectID !== input.project.id ||
|
|
107
|
+
conversation.session.parentID)
|
|
108
|
+
return 'Skipped session outside the current top-level project.';
|
|
109
|
+
const result = await archiveConversation(conversation, paths);
|
|
110
|
+
return result.file
|
|
111
|
+
? `Archived ${result.turns} turn(s) to ${result.file}`
|
|
112
|
+
: 'No visible conversation content to archive.';
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
conversation_archive_backfill: tool({
|
|
116
|
+
description: 'Archive all existing top-level OpenCode sessions visible in the current project, then commit and push once.',
|
|
117
|
+
args: {},
|
|
118
|
+
execute: async (_args, context) => {
|
|
119
|
+
await context.ask({
|
|
120
|
+
permission: 'conversation-archive-backfill',
|
|
121
|
+
patterns: ['all sessions in current OpenCode project'],
|
|
122
|
+
always: [],
|
|
123
|
+
metadata: {},
|
|
124
|
+
});
|
|
125
|
+
const listed = await input.client.session.list({ throwOnError: true });
|
|
126
|
+
const sessions = (listed.data || []);
|
|
127
|
+
let archived = 0;
|
|
128
|
+
let skipped = 0;
|
|
129
|
+
for (const session of sessions) {
|
|
130
|
+
// OpenCode's list endpoint can include unrelated projects; the tool
|
|
131
|
+
// promises a current-project backfill and must not cross that boundary.
|
|
132
|
+
if (session.projectID !== input.project.id || session.parentID) {
|
|
133
|
+
skipped += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const result = await archiveConversation(await fetchConversation(input, session.id), paths, false);
|
|
138
|
+
if (result.file)
|
|
139
|
+
archived += 1;
|
|
140
|
+
else
|
|
141
|
+
skipped += 1;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
skipped += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const commit = await commitPending(paths, `backfill ${archived} session(s): ${new Date().toLocaleString()}`);
|
|
148
|
+
if (commit.error)
|
|
149
|
+
return `Backfilled ${archived} session(s), skipped ${skipped}. Commit failed: ${commit.error}`;
|
|
150
|
+
const pushed = await upload(paths);
|
|
151
|
+
return `Backfilled ${archived} session(s), skipped ${skipped}. ${pushed}`;
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
export default {
|
|
158
|
+
id: 'genspark.opencode-conversation-archiver',
|
|
159
|
+
server,
|
|
160
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@genspark/opencode-conversation-archiver",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Archive OpenCode conversations as readable Markdown in a git repository",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"opencode",
|
|
7
|
+
"conversation",
|
|
8
|
+
"archive",
|
|
9
|
+
"git",
|
|
10
|
+
"genspark"
|
|
11
|
+
],
|
|
12
|
+
"author": "Genspark",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/wework5119/gen-spark.git",
|
|
17
|
+
"directory": "toolkits/opencode-conversation-archiver"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/wework5119/gen-spark/tree/main/toolkits/opencode-conversation-archiver#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/wework5119/gen-spark/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/server.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
"./server": {
|
|
27
|
+
"types": "./dist/server.d.ts",
|
|
28
|
+
"import": "./dist/server.js",
|
|
29
|
+
"config": {
|
|
30
|
+
"mode": "auto"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"bin": {
|
|
39
|
+
"opencode-conversation-archiver": "./dist/cli.js"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"lint": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
47
|
+
"prepublishOnly": "npm run lint && npm test && npm run build"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20.0.0",
|
|
54
|
+
"opencode": ">=1.17.18 <2"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@opencode-ai/plugin": "^1.17.18"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@opencode-ai/plugin": "1.17.18",
|
|
61
|
+
"@opencode-ai/sdk": "1.17.18",
|
|
62
|
+
"@types/bun": "latest",
|
|
63
|
+
"typescript": "^5.8.2",
|
|
64
|
+
"vitest": "^4.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|