@clinebot/core 0.0.14 → 0.0.15
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/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +100 -96
- package/dist/session/default-session-manager.d.ts +2 -1
- package/dist/session/file-session-service.d.ts +5 -0
- package/dist/session/session-host.d.ts +2 -1
- package/dist/storage/file-team-store.d.ts +27 -0
- package/dist/storage/team-store.d.ts +13 -0
- package/package.json +4 -4
- package/src/account/cline-account-service.ts +7 -0
- package/src/account/types.ts +6 -0
- package/src/index.node.ts +38 -0
- package/src/runtime/runtime-builder.team-persistence.test.ts +4 -5
- package/src/runtime/runtime-builder.ts +2 -3
- package/src/session/default-session-manager.ts +5 -1
- package/src/session/file-session-service.ts +280 -0
- package/src/session/session-host.test.ts +29 -0
- package/src/session/session-host.ts +17 -3
- package/src/session/session-team-coordination.ts +6 -5
- package/src/storage/file-team-store.ts +257 -0
- package/src/storage/team-store.ts +35 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import type {
|
|
12
|
+
TeamEvent,
|
|
13
|
+
TeamRuntimeState,
|
|
14
|
+
TeamTeammateSpec,
|
|
15
|
+
} from "@clinebot/agents";
|
|
16
|
+
import { resolveTeamDataDir } from "@clinebot/shared/storage";
|
|
17
|
+
import type { TeamStore } from "../types/storage";
|
|
18
|
+
|
|
19
|
+
function nowIso(): string {
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sanitizeTeamName(name: string): string {
|
|
24
|
+
return name
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
27
|
+
.replace(/^-+|-+$/g, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function reviveTeamRuntimeStateDates(
|
|
31
|
+
state: TeamRuntimeState,
|
|
32
|
+
): TeamRuntimeState {
|
|
33
|
+
return {
|
|
34
|
+
...state,
|
|
35
|
+
tasks: state.tasks.map((task) => ({
|
|
36
|
+
...task,
|
|
37
|
+
createdAt: new Date(task.createdAt),
|
|
38
|
+
updatedAt: new Date(task.updatedAt),
|
|
39
|
+
})),
|
|
40
|
+
mailbox: state.mailbox.map((message) => ({
|
|
41
|
+
...message,
|
|
42
|
+
sentAt: new Date(message.sentAt),
|
|
43
|
+
readAt: message.readAt ? new Date(message.readAt) : undefined,
|
|
44
|
+
})),
|
|
45
|
+
missionLog: state.missionLog.map((entry) => ({
|
|
46
|
+
...entry,
|
|
47
|
+
ts: new Date(entry.ts),
|
|
48
|
+
})),
|
|
49
|
+
runs: (state.runs ?? []).map((run) => ({
|
|
50
|
+
...run,
|
|
51
|
+
startedAt: new Date(run.startedAt),
|
|
52
|
+
endedAt: run.endedAt ? new Date(run.endedAt) : undefined,
|
|
53
|
+
nextAttemptAt: run.nextAttemptAt
|
|
54
|
+
? new Date(run.nextAttemptAt)
|
|
55
|
+
: undefined,
|
|
56
|
+
heartbeatAt: run.heartbeatAt ? new Date(run.heartbeatAt) : undefined,
|
|
57
|
+
})),
|
|
58
|
+
outcomes: (state.outcomes ?? []).map((outcome) => ({
|
|
59
|
+
...outcome,
|
|
60
|
+
createdAt: new Date(outcome.createdAt),
|
|
61
|
+
finalizedAt: outcome.finalizedAt
|
|
62
|
+
? new Date(outcome.finalizedAt)
|
|
63
|
+
: undefined,
|
|
64
|
+
})),
|
|
65
|
+
outcomeFragments: (state.outcomeFragments ?? []).map((fragment) => ({
|
|
66
|
+
...fragment,
|
|
67
|
+
createdAt: new Date(fragment.createdAt),
|
|
68
|
+
reviewedAt: fragment.reviewedAt
|
|
69
|
+
? new Date(fragment.reviewedAt)
|
|
70
|
+
: undefined,
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PersistedTeamEnvelope {
|
|
76
|
+
version: 1;
|
|
77
|
+
updatedAt: string;
|
|
78
|
+
teamState: TeamRuntimeState;
|
|
79
|
+
teammates: TeamTeammateSpec[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FileTeamStoreOptions {
|
|
83
|
+
teamDir?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TeamRuntimeLoadResult {
|
|
87
|
+
state?: TeamRuntimeState;
|
|
88
|
+
teammates: TeamTeammateSpec[];
|
|
89
|
+
interruptedRunIds: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class FileTeamStore implements TeamStore {
|
|
93
|
+
private readonly teamDirPath: string;
|
|
94
|
+
|
|
95
|
+
constructor(options: FileTeamStoreOptions = {}) {
|
|
96
|
+
this.teamDirPath = options.teamDir ?? resolveTeamDataDir();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
init(): void {
|
|
100
|
+
this.ensureTeamDir();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
listTeamNames(): string[] {
|
|
104
|
+
if (!existsSync(this.teamDirPath)) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return readdirSync(this.teamDirPath, { withFileTypes: true })
|
|
108
|
+
.filter((entry) => entry.isDirectory())
|
|
109
|
+
.filter((entry) => existsSync(this.statePath(entry.name)))
|
|
110
|
+
.map((entry) => entry.name)
|
|
111
|
+
.sort();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
readState(teamName: string): TeamRuntimeState | undefined {
|
|
115
|
+
const envelope = this.readEnvelope(teamName);
|
|
116
|
+
return envelope?.teamState
|
|
117
|
+
? reviveTeamRuntimeStateDates(envelope.teamState)
|
|
118
|
+
: undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
readHistory(teamName: string, limit = 200): unknown[] {
|
|
122
|
+
const historyPath = this.historyPath(teamName);
|
|
123
|
+
if (!existsSync(historyPath)) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
return readFileSync(historyPath, "utf8")
|
|
127
|
+
.split("\n")
|
|
128
|
+
.map((line) => line.trim())
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
.map((line) => {
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(line) as unknown;
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
.filter((item): item is unknown => item !== undefined)
|
|
138
|
+
.reverse()
|
|
139
|
+
.slice(0, limit);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
loadRuntime(teamName: string): TeamRuntimeLoadResult {
|
|
143
|
+
const envelope = this.readEnvelope(teamName);
|
|
144
|
+
const interruptedRunIds = this.markInProgressRunsInterrupted(
|
|
145
|
+
teamName,
|
|
146
|
+
"runtime_recovered",
|
|
147
|
+
);
|
|
148
|
+
return {
|
|
149
|
+
state: envelope?.teamState
|
|
150
|
+
? reviveTeamRuntimeStateDates(envelope.teamState)
|
|
151
|
+
: undefined,
|
|
152
|
+
teammates: envelope?.teammates ?? [],
|
|
153
|
+
interruptedRunIds,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
handleTeamEvent(teamName: string, event: TeamEvent): void {
|
|
158
|
+
this.ensureTeamSubdir(teamName);
|
|
159
|
+
appendFileSync(
|
|
160
|
+
this.historyPath(teamName),
|
|
161
|
+
`${JSON.stringify({ ts: nowIso(), eventType: event.type, payload: event })}\n`,
|
|
162
|
+
"utf8",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
persistRuntime(
|
|
167
|
+
teamName: string,
|
|
168
|
+
state: TeamRuntimeState,
|
|
169
|
+
teammates: TeamTeammateSpec[],
|
|
170
|
+
): void {
|
|
171
|
+
this.ensureTeamSubdir(teamName);
|
|
172
|
+
const envelope: PersistedTeamEnvelope = {
|
|
173
|
+
version: 1,
|
|
174
|
+
updatedAt: nowIso(),
|
|
175
|
+
teamState: state,
|
|
176
|
+
teammates,
|
|
177
|
+
};
|
|
178
|
+
const path = this.statePath(teamName);
|
|
179
|
+
const tempPath = `${path}.tmp`;
|
|
180
|
+
writeFileSync(tempPath, `${JSON.stringify(envelope, null, 2)}\n`, "utf8");
|
|
181
|
+
renameSync(tempPath, path);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
markInProgressRunsInterrupted(teamName: string, reason: string): string[] {
|
|
185
|
+
const envelope = this.readEnvelope(teamName);
|
|
186
|
+
if (!envelope?.teamState?.runs?.length) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const interrupted = envelope.teamState.runs
|
|
190
|
+
.filter((run) => run.status === "queued" || run.status === "running")
|
|
191
|
+
.map((run) => run.id);
|
|
192
|
+
if (interrupted.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const endedAt = new Date();
|
|
196
|
+
envelope.teamState = {
|
|
197
|
+
...envelope.teamState,
|
|
198
|
+
runs: envelope.teamState.runs.map((run) =>
|
|
199
|
+
run.status === "queued" || run.status === "running"
|
|
200
|
+
? {
|
|
201
|
+
...run,
|
|
202
|
+
status: "interrupted",
|
|
203
|
+
error: reason,
|
|
204
|
+
endedAt,
|
|
205
|
+
}
|
|
206
|
+
: run,
|
|
207
|
+
),
|
|
208
|
+
};
|
|
209
|
+
this.persistRuntime(teamName, envelope.teamState, envelope.teammates);
|
|
210
|
+
return interrupted;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private ensureTeamDir(): string {
|
|
214
|
+
if (!existsSync(this.teamDirPath)) {
|
|
215
|
+
mkdirSync(this.teamDirPath, { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
return this.teamDirPath;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private ensureTeamSubdir(teamName: string): string {
|
|
221
|
+
const path = join(this.ensureTeamDir(), sanitizeTeamName(teamName));
|
|
222
|
+
if (!existsSync(path)) {
|
|
223
|
+
mkdirSync(path, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
return path;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private statePath(teamName: string): string {
|
|
229
|
+
return join(this.ensureTeamDir(), sanitizeTeamName(teamName), "state.json");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private historyPath(teamName: string): string {
|
|
233
|
+
return join(
|
|
234
|
+
this.ensureTeamDir(),
|
|
235
|
+
sanitizeTeamName(teamName),
|
|
236
|
+
"task-history.jsonl",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private readEnvelope(teamName: string): PersistedTeamEnvelope | undefined {
|
|
241
|
+
const path = this.statePath(teamName);
|
|
242
|
+
if (!existsSync(path)) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(
|
|
247
|
+
readFileSync(path, "utf8"),
|
|
248
|
+
) as PersistedTeamEnvelope;
|
|
249
|
+
if (parsed?.version === 1 && parsed.teamState) {
|
|
250
|
+
return parsed;
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// Ignore invalid persistence and fall back to undefined.
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
export type { TeamStore } from "../types/storage";
|
|
2
2
|
export {
|
|
3
|
+
FileTeamStore,
|
|
4
|
+
type FileTeamStoreOptions,
|
|
5
|
+
} from "./file-team-store";
|
|
6
|
+
export {
|
|
7
|
+
SqliteTeamStore,
|
|
8
|
+
type SqliteTeamStoreOptions,
|
|
9
|
+
} from "./sqlite-team-store";
|
|
10
|
+
|
|
11
|
+
import { FileTeamStore } from "./file-team-store";
|
|
12
|
+
import {
|
|
3
13
|
SqliteTeamStore,
|
|
4
14
|
type SqliteTeamStoreOptions,
|
|
5
15
|
} from "./sqlite-team-store";
|
|
16
|
+
|
|
17
|
+
export function createLocalTeamStore(options: SqliteTeamStoreOptions = {}): {
|
|
18
|
+
init(): void;
|
|
19
|
+
listTeamNames(): string[];
|
|
20
|
+
readState(teamName: string): ReturnType<FileTeamStore["readState"]>;
|
|
21
|
+
readHistory(teamName: string, limit?: number): unknown[];
|
|
22
|
+
loadRuntime(teamName: string): ReturnType<FileTeamStore["loadRuntime"]>;
|
|
23
|
+
handleTeamEvent: FileTeamStore["handleTeamEvent"];
|
|
24
|
+
persistRuntime: FileTeamStore["persistRuntime"];
|
|
25
|
+
markInProgressRunsInterrupted: FileTeamStore["markInProgressRunsInterrupted"];
|
|
26
|
+
} {
|
|
27
|
+
try {
|
|
28
|
+
const store = new SqliteTeamStore(options);
|
|
29
|
+
store.init();
|
|
30
|
+
return store;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn(
|
|
33
|
+
"SQLite team persistence unavailable, falling back to file-based team storage.",
|
|
34
|
+
error,
|
|
35
|
+
);
|
|
36
|
+
const store = new FileTeamStore({ teamDir: options.teamDir });
|
|
37
|
+
store.init();
|
|
38
|
+
return store;
|
|
39
|
+
}
|
|
40
|
+
}
|