@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0-preview.20260912
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/gui/dist/assets/index-D_t6sCWs.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { type IntegrationIO, type IntegrationTransaction, type ReadResult, type StatKind } from "./config-io";
|
|
3
|
+
import type { IntegrationStateStore } from "./store";
|
|
4
|
+
import { fingerprint, type OwnershipRecord } from "./ownership";
|
|
5
|
+
import type { JournalEntry } from "./journal";
|
|
6
|
+
import { parseClineTransaction } from "./cline-transaction";
|
|
7
|
+
import { decodeClinePair, encodeClinePair, type ClineRawPair } from "./cline-document";
|
|
8
|
+
|
|
9
|
+
/** Per-file atomic replacement with durable pair recovery, not simultaneous two-file visibility. */
|
|
10
|
+
export class ClineTransactionError extends Error {
|
|
11
|
+
readonly residual = true;
|
|
12
|
+
constructor(readonly snapshotPath: string) {
|
|
13
|
+
super(`Cline has an unfinished two-file operation. Stop Cline and retry the mutation. If either file was edited, recover from ${snapshotPath} before retrying.`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sameRecord(a: unknown, b: unknown): boolean {
|
|
18
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function clinePendingPath(store: IntegrationStateStore, configPath: string): string {
|
|
22
|
+
return join(store.root, `cline-pending-${fingerprint(resolve(configPath))}.json`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Snapshot/ownership remain in the existing store. Only the filesystem projection changes. */
|
|
26
|
+
export function createClineIO(
|
|
27
|
+
base: IntegrationIO,
|
|
28
|
+
configPath: string,
|
|
29
|
+
store: IntegrationStateStore,
|
|
30
|
+
recover = false,
|
|
31
|
+
): IntegrationIO {
|
|
32
|
+
const catalogPath = join(dirname(configPath), "models.json");
|
|
33
|
+
const markerPath = clinePendingPath(store, configPath);
|
|
34
|
+
const targets = { settings: configPath, catalog: catalogPath };
|
|
35
|
+
const keys = ["settings", "catalog"] as const;
|
|
36
|
+
const kind = (path: string) => (base.lstatKind ?? base.statKind)(path);
|
|
37
|
+
const readPair = (): ClineRawPair => {
|
|
38
|
+
const pair: ClineRawPair = { settings: null, catalog: null };
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
const path = targets[key];
|
|
41
|
+
const state = kind(path);
|
|
42
|
+
if (state === "missing") continue;
|
|
43
|
+
if (state !== "file") throw new ClineTransactionError(markerPath);
|
|
44
|
+
const read = base.readText(path);
|
|
45
|
+
if (read.kind !== "text") throw new ClineTransactionError(markerPath);
|
|
46
|
+
pair[key] = read.text;
|
|
47
|
+
}
|
|
48
|
+
return pair;
|
|
49
|
+
};
|
|
50
|
+
const writeMember = (key: typeof keys[number], text: string | null): void => {
|
|
51
|
+
if (text === null) base.removeFile(targets[key]);
|
|
52
|
+
else { base.mkdirp(dirname(targets[key])); base.writeText(targets[key], text); }
|
|
53
|
+
};
|
|
54
|
+
const writePair = (pair: ClineRawPair): void => {
|
|
55
|
+
for (const key of keys) writeMember(key, pair[key]);
|
|
56
|
+
};
|
|
57
|
+
const readPending = (): IntegrationTransaction | null => {
|
|
58
|
+
if (kind(markerPath) === "missing") return null;
|
|
59
|
+
if (kind(markerPath) !== "file") throw new ClineTransactionError(markerPath);
|
|
60
|
+
const read = base.readText(markerPath);
|
|
61
|
+
if (read.kind !== "text") throw new ClineTransactionError(markerPath);
|
|
62
|
+
try {
|
|
63
|
+
return parseClineTransaction(read.text, configPath);
|
|
64
|
+
} catch { throw new ClineTransactionError(markerPath); }
|
|
65
|
+
};
|
|
66
|
+
const committed = (pending: IntegrationTransaction, current: ClineRawPair): boolean => {
|
|
67
|
+
let operation: JournalEntry | null;
|
|
68
|
+
let record: OwnershipRecord | undefined;
|
|
69
|
+
try { operation = store.findCommittedOperation(pending.entry.opId); record = store.readRecordsStrict().cline; }
|
|
70
|
+
catch { throw new ClineTransactionError(markerPath); }
|
|
71
|
+
return operation !== null && operation.clientId === "cline" && operation.configPath === configPath
|
|
72
|
+
&& operation.resultFingerprint === pending.entry.resultFingerprint
|
|
73
|
+
&& encodeClinePair(current) === pending.nextText
|
|
74
|
+
&& sameRecord(record, pending.record);
|
|
75
|
+
};
|
|
76
|
+
const cleanup = (): void => {
|
|
77
|
+
// The append is the commit boundary. Cleanup failure never rolls back a committed pair.
|
|
78
|
+
try { base.removeFile(markerPath); } catch { /* The next mutation recognizes the committed record. */ }
|
|
79
|
+
};
|
|
80
|
+
const pendingSafe = (): boolean => {
|
|
81
|
+
const pending = readPending();
|
|
82
|
+
return pending === null || committed(pending, readPair());
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (resolve(configPath).toLowerCase() === resolve(catalogPath).toLowerCase()) throw new ClineTransactionError(markerPath);
|
|
86
|
+
if (recover) {
|
|
87
|
+
const pending = readPending();
|
|
88
|
+
if (pending) {
|
|
89
|
+
const current = readPair();
|
|
90
|
+
if (committed(pending, current)) cleanup();
|
|
91
|
+
else {
|
|
92
|
+
// A journaled but mixed/inconsistent result is not an interrupted uncommitted write.
|
|
93
|
+
try {
|
|
94
|
+
if (store.findCommittedOperation(pending.entry.opId)) throw new ClineTransactionError(markerPath);
|
|
95
|
+
} catch { throw new ClineTransactionError(markerPath); }
|
|
96
|
+
const before = decodeClinePair(pending.before);
|
|
97
|
+
const after = decodeClinePair(pending.nextText);
|
|
98
|
+
if (!keys.every(key => current[key] === before[key] || current[key] === after[key])) throw new ClineTransactionError(markerPath);
|
|
99
|
+
let record: OwnershipRecord | null;
|
|
100
|
+
try { record = store.readRecordsStrict().cline ?? null; }
|
|
101
|
+
catch { throw new ClineTransactionError(markerPath); }
|
|
102
|
+
if (!sameRecord(record, pending.priorRecord) && !sameRecord(record, pending.record)) throw new ClineTransactionError(markerPath);
|
|
103
|
+
try {
|
|
104
|
+
writePair(before);
|
|
105
|
+
if (pending.priorRecord) base.putRecord(pending.priorRecord);
|
|
106
|
+
else base.dropRecord("cline");
|
|
107
|
+
base.removeFile(markerPath);
|
|
108
|
+
} catch { throw new ClineTransactionError(markerPath); }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
statKind: (path): StatKind => {
|
|
116
|
+
if (path !== configPath) return base.statKind(path);
|
|
117
|
+
try {
|
|
118
|
+
if (!pendingSafe()) return "failed";
|
|
119
|
+
const pair = readPair();
|
|
120
|
+
return pair.settings === null && pair.catalog === null ? "missing" : "file";
|
|
121
|
+
} catch { return "failed"; }
|
|
122
|
+
},
|
|
123
|
+
readText: (path): ReadResult => {
|
|
124
|
+
if (path !== configPath) return base.readText(path);
|
|
125
|
+
try {
|
|
126
|
+
if (!pendingSafe()) return { kind: "failed", code: "CLINE_PENDING" };
|
|
127
|
+
const text = encodeClinePair(readPair());
|
|
128
|
+
return text === null ? { kind: "missing" } : { kind: "text", text };
|
|
129
|
+
} catch { return { kind: "failed", code: "CLINE_UNSAFE" }; }
|
|
130
|
+
},
|
|
131
|
+
beginTransaction: transaction => {
|
|
132
|
+
if (transaction.entry.clientId !== "cline" || transaction.entry.configPath !== configPath) throw new ClineTransactionError(markerPath);
|
|
133
|
+
if (readPending() !== null) throw new ClineTransactionError(markerPath);
|
|
134
|
+
if (encodeClinePair(readPair()) !== transaction.before) throw new ClineTransactionError(markerPath);
|
|
135
|
+
base.mkdirp(store.root);
|
|
136
|
+
const { entry, before, nextText, record, priorRecord } = transaction;
|
|
137
|
+
base.writeText(markerPath, JSON.stringify({ entry, before, nextText, record, priorRecord }));
|
|
138
|
+
},
|
|
139
|
+
writeText: (path, text) => {
|
|
140
|
+
if (path !== configPath) return base.writeText(path, text);
|
|
141
|
+
writePair(decodeClinePair(text));
|
|
142
|
+
},
|
|
143
|
+
removeFile: path => {
|
|
144
|
+
if (path !== configPath) return base.removeFile(path);
|
|
145
|
+
writePair({ settings: null, catalog: null });
|
|
146
|
+
},
|
|
147
|
+
finishTransaction: cleanup,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IntegrationTransaction } from "./config-io";
|
|
2
|
+
import { decodeClinePair, encodeClinePair, isClineObject } from "./cline-document";
|
|
3
|
+
import { isJournalEntry } from "./journal";
|
|
4
|
+
import { fingerprint, isOwnershipRecord, type OwnershipRecord } from "./ownership";
|
|
5
|
+
import { validRefreshablePaths } from "./ownership-policy";
|
|
6
|
+
|
|
7
|
+
const FRAGMENTS = [["catalog", "providers", "opencodex"], ["settings", "providers", "opencodex"]];
|
|
8
|
+
const CONTAINERS = new Set(["catalog", "settings", "catalog\0providers", "settings\0providers"]);
|
|
9
|
+
|
|
10
|
+
function clineRecord(value: unknown, configPath: string): value is OwnershipRecord | null {
|
|
11
|
+
if (value === null) return true;
|
|
12
|
+
if (!isOwnershipRecord(value) || value.clientId !== "cline" || value.configPath !== configPath) return false;
|
|
13
|
+
if (value.fragmentPaths.length !== FRAGMENTS.length
|
|
14
|
+
|| !FRAGMENTS.every(expected => value.fragmentPaths.some(path =>
|
|
15
|
+
path.length === expected.length && path.every((segment, index) => segment === expected[index])))) return false;
|
|
16
|
+
if (value.createdContainers?.some(path => !CONTAINERS.has(path))) return false;
|
|
17
|
+
if (value.refreshablePaths !== undefined
|
|
18
|
+
&& !validRefreshablePaths({ clientId: "cline", fragments: [] }, value.refreshablePaths)) return false;
|
|
19
|
+
return value.protectedBlockFingerprint === undefined || value.refreshablePaths !== undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Validate all recovery authority before a file or ownership record can be replaced. */
|
|
23
|
+
export function parseClineTransaction(text: string, configPath: string): IntegrationTransaction {
|
|
24
|
+
const value: unknown = JSON.parse(text);
|
|
25
|
+
if (!isClineObject(value) || !isJournalEntry(value.entry)
|
|
26
|
+
|| value.entry.clientId !== "cline" || value.entry.configPath !== configPath
|
|
27
|
+
|| !(typeof value.before === "string" || value.before === null)
|
|
28
|
+
|| !(typeof value.nextText === "string" || value.nextText === null)
|
|
29
|
+
|| !clineRecord(value.record, configPath) || !clineRecord(value.priorRecord, configPath)
|
|
30
|
+
|| !clineRecord(value.entry.priorRecord, configPath)
|
|
31
|
+
|| JSON.stringify(value.entry.priorRecord) !== JSON.stringify(value.priorRecord)
|
|
32
|
+
|| (value.record !== null && (value.record.opId !== value.entry.opId || value.record.appliedAt !== value.entry.at))) {
|
|
33
|
+
throw new Error("invalid Cline transaction metadata");
|
|
34
|
+
}
|
|
35
|
+
if (encodeClinePair(decodeClinePair(value.before)) !== value.before
|
|
36
|
+
|| encodeClinePair(decodeClinePair(value.nextText)) !== value.nextText
|
|
37
|
+
|| value.entry.resultAbsent !== (value.nextText === null)
|
|
38
|
+
|| value.entry.resultFingerprint !== (value.nextText === null ? "" : fingerprint(value.nextText))) {
|
|
39
|
+
throw new Error("inconsistent Cline transaction result");
|
|
40
|
+
}
|
|
41
|
+
return { entry: value.entry, before: value.before, nextText: value.nextText, record: value.record, priorRecord: value.priorRecord };
|
|
42
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Design of record: devlog/_fin/260802_client_toggle_api/021 §5-6.
|
|
9
9
|
*/
|
|
10
|
-
import { mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
10
|
+
import { lstatSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import type { ConfigFormat } from "../clients/config-export";
|
|
12
12
|
import { MAX_JSON_NESTING } from "./serialize";
|
|
13
13
|
import { atomicWriteFile } from "../config";
|
|
@@ -200,6 +200,8 @@ export interface IntegrationIO {
|
|
|
200
200
|
readText: (path: string) => ReadResult;
|
|
201
201
|
/** `failed` is distinct from `missing` for the same reason. */
|
|
202
202
|
statKind: (path: string) => StatKind;
|
|
203
|
+
/** Optional no-follow probe for paired-file clients. */
|
|
204
|
+
lstatKind?: (path: string) => StatKind;
|
|
203
205
|
writeText: (path: string, text: string) => void;
|
|
204
206
|
removeFile: (path: string) => void;
|
|
205
207
|
mkdirp: (path: string) => void;
|
|
@@ -208,6 +210,16 @@ export interface IntegrationIO {
|
|
|
208
210
|
appendJournal: (entry: JournalEntry) => void;
|
|
209
211
|
putRecord: (record: OwnershipRecord) => void;
|
|
210
212
|
dropRecord: (clientId: IntegrationClientId) => void;
|
|
213
|
+
beginTransaction?: (transaction: IntegrationTransaction) => void;
|
|
214
|
+
finishTransaction?: () => void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface IntegrationTransaction {
|
|
218
|
+
entry: JournalEntry;
|
|
219
|
+
before: string | null;
|
|
220
|
+
nextText: string | null;
|
|
221
|
+
record: OwnershipRecord | null;
|
|
222
|
+
priorRecord: OwnershipRecord | null;
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
export type TargetState =
|
|
@@ -240,6 +252,14 @@ export function loadTarget(io: IntegrationIO, configPath: string): TargetState {
|
|
|
240
252
|
*/
|
|
241
253
|
export function fileIO(): Omit<IntegrationIO, "appendJournal" | "putRecord" | "dropRecord"> {
|
|
242
254
|
return {
|
|
255
|
+
lstatKind: path => {
|
|
256
|
+
try {
|
|
257
|
+
const stats = lstatSync(path);
|
|
258
|
+
return stats.isFile() ? "file" : stats.isDirectory() ? "dir" : "other";
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT" ? "missing" : "failed";
|
|
261
|
+
}
|
|
262
|
+
},
|
|
243
263
|
readText: path => {
|
|
244
264
|
try {
|
|
245
265
|
return { kind: "text", text: readFileSync(path, "utf8") };
|
|
@@ -334,4 +354,3 @@ export function assertIntegrationWriteOwnership(
|
|
|
334
354
|
+ "of sharing the mount.",
|
|
335
355
|
);
|
|
336
356
|
}
|
|
337
|
-
|
|
@@ -16,7 +16,7 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
import { appendFileSync, existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
17
17
|
import { join, resolve, sep } from "node:path";
|
|
18
18
|
import { atomicWriteFile } from "../config";
|
|
19
|
-
import { ensureDir, fingerprint, integrationsDir, type OwnershipRecord } from "./ownership";
|
|
19
|
+
import { ensureDir, fingerprint, integrationsDir, isOwnershipRecord, type OwnershipRecord } from "./ownership";
|
|
20
20
|
import { isIntegrationClientId, type IntegrationClientId } from "./registry";
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -231,6 +231,48 @@ export function findOperation(opId: string, dir: string = integrationsDir()): Jo
|
|
|
231
231
|
return listOperations(undefined, Number.MAX_SAFE_INTEGER, dir).find(row => row.opId === opId) ?? null;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
export function isJournalEntry(value: unknown): value is JournalEntry {
|
|
235
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
236
|
+
const entry = value as Record<string, unknown>;
|
|
237
|
+
const snapshot = entry.snapshot;
|
|
238
|
+
return typeof entry.opId === "string" && /^[a-zA-Z0-9-]+$/.test(entry.opId)
|
|
239
|
+
&& typeof entry.clientId === "string" && isIntegrationClientId(entry.clientId)
|
|
240
|
+
&& typeof entry.configPath === "string" && entry.configPath.length > 0
|
|
241
|
+
&& typeof entry.at === "string" && Number.isFinite(Date.parse(entry.at))
|
|
242
|
+
&& typeof entry.kind === "string" && ["apply", "refresh", "disable", "restore", "overwrite"].includes(entry.kind)
|
|
243
|
+
&& typeof entry.resultAbsent === "boolean"
|
|
244
|
+
&& (entry.resultAbsent ? entry.resultFingerprint === "" : typeof entry.resultFingerprint === "string" && /^[a-f0-9]{16}$/.test(entry.resultFingerprint))
|
|
245
|
+
&& (entry.priorRecord === null || isOwnershipRecord(entry.priorRecord))
|
|
246
|
+
&& snapshot !== null && typeof snapshot === "object" && !Array.isArray(snapshot)
|
|
247
|
+
&& ("kind" in snapshot && (snapshot.kind === "none" || snapshot.kind === "expired"
|
|
248
|
+
|| (snapshot.kind === "stored" && "relPath" in snapshot && typeof snapshot.relPath === "string" && snapshot.relPath.length > 0)));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Commit evidence includes retired rows. Read/parse failures must never become "not committed". */
|
|
252
|
+
export function findCommittedOperation(opId: string, dir: string = integrationsDir()): JournalEntry | null {
|
|
253
|
+
let raw: string;
|
|
254
|
+
try { raw = readFileSync(journalPath(dir), "utf8"); }
|
|
255
|
+
catch (error) {
|
|
256
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
257
|
+
throw new Error("integration journal cannot be read for recovery");
|
|
258
|
+
}
|
|
259
|
+
let found: JournalEntry | null = null;
|
|
260
|
+
for (const line of raw.split("\n")) {
|
|
261
|
+
if (!line.trim()) continue;
|
|
262
|
+
let row: unknown;
|
|
263
|
+
try { row = JSON.parse(line); }
|
|
264
|
+
catch { throw new Error("integration journal is incomplete for recovery"); }
|
|
265
|
+
if (isTombstone(row)) {
|
|
266
|
+
if (!row.tombstone || typeof row.at !== "string" || !Number.isFinite(Date.parse(row.at))
|
|
267
|
+
|| typeof row.by !== "string" || !row.by) throw new Error("integration journal contains invalid recovery metadata");
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (!isJournalEntry(row)) throw new Error("integration journal contains invalid recovery metadata");
|
|
271
|
+
if (row.opId === opId) found = row;
|
|
272
|
+
}
|
|
273
|
+
return found;
|
|
274
|
+
}
|
|
275
|
+
|
|
234
276
|
/** Resolves the tag against what is actually on disk now. */
|
|
235
277
|
export function readSnapshot(
|
|
236
278
|
entry: JournalEntry,
|
|
@@ -29,7 +29,20 @@ interface MissingEntry {
|
|
|
29
29
|
insertAt: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
interface ReplaceLineEntry {
|
|
33
|
+
lines: readonly SourceLine[];
|
|
34
|
+
index: number;
|
|
35
|
+
indent: number;
|
|
36
|
+
missingDepth: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type LocatedPath =
|
|
40
|
+
| { kind: "existing"; entry: LocatedEntry }
|
|
41
|
+
| { kind: "missing"; entry: MissingEntry }
|
|
42
|
+
// `key: {}` — an empty inline map the block-key scanner cannot see (#4260).
|
|
43
|
+
| { kind: "replace-line"; entry: ReplaceLineEntry }
|
|
44
|
+
// A populated flow container. Still refused, but nameable as its own cause.
|
|
45
|
+
| { kind: "unsupported-style" };
|
|
33
46
|
|
|
34
47
|
export type YamlFragmentMutation =
|
|
35
48
|
| { kind: "upsert"; value: unknown }
|
|
@@ -82,6 +95,61 @@ function isPlainBlockKey(line: string, indent: number, key: string): boolean {
|
|
|
82
95
|
return new RegExp(`^${regexpEscape(key)}:[ ]*(?:#.*)?$`, "u").test(rest);
|
|
83
96
|
}
|
|
84
97
|
|
|
98
|
+
/** The inline value written after `key:` on this line, or null if the key is not here. */
|
|
99
|
+
function inlineValueAfterKey(line: string, indent: number, key: string): string | null {
|
|
100
|
+
const spaces = leadingSpaces(line);
|
|
101
|
+
if (spaces !== indent) return null;
|
|
102
|
+
const rest = line.slice(indent);
|
|
103
|
+
const head = `${key}:`;
|
|
104
|
+
// Compared as text, not as a pattern: a path segment is arbitrary user data,
|
|
105
|
+
// and brace escaping inside a `u`-flag regex is its own hazard.
|
|
106
|
+
if (!rest.startsWith(head)) return null;
|
|
107
|
+
return rest.slice(head.length).trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Exactly `key: {}` (any inner spacing) — an empty inline map, no inline comment. */
|
|
111
|
+
function isEmptyInlineMapKey(line: string, indent: number, key: string): boolean {
|
|
112
|
+
const value = inlineValueAfterKey(line, indent, key);
|
|
113
|
+
if (value === null) return false;
|
|
114
|
+
return value.startsWith("{") && value.endsWith("}") && value.slice(1, -1).trim().length === 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `key: { ... }` or `key: [ ... ]` on one line: content we would have to re-render. */
|
|
118
|
+
function isPopulatedInlineFlowKey(line: string, indent: number, key: string): boolean {
|
|
119
|
+
const value = inlineValueAfterKey(line, indent, key);
|
|
120
|
+
if (value === null) return false;
|
|
121
|
+
if (!value.startsWith("{") && !value.startsWith("[")) return false;
|
|
122
|
+
return !isEmptyInlineMapKey(line, indent, key);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A plain block key whose first child opens a flow collection:
|
|
127
|
+
*
|
|
128
|
+
* providers:
|
|
129
|
+
* { native: { ... } }
|
|
130
|
+
*
|
|
131
|
+
* DSH writes this shape itself. The walk passes straight through it — the key
|
|
132
|
+
* line is a plain block key and `containerEnd` does not stop at `}` — so the
|
|
133
|
+
* refusal used to surface only as a failed re-parse at the very end and got
|
|
134
|
+
* reported as a comment or formatting problem that was not there (#4260).
|
|
135
|
+
*/
|
|
136
|
+
function firstChildOpensFlow(
|
|
137
|
+
lines: readonly SourceLine[],
|
|
138
|
+
start: number,
|
|
139
|
+
end: number,
|
|
140
|
+
parentIndent: number,
|
|
141
|
+
): boolean {
|
|
142
|
+
for (let index = start + 1; index < end; index += 1) {
|
|
143
|
+
const body = lines[index]!.body;
|
|
144
|
+
if (isBlank(body) || isComment(body)) continue;
|
|
145
|
+
const spaces = leadingSpaces(body);
|
|
146
|
+
if (spaces === null || spaces <= parentIndent) continue;
|
|
147
|
+
const trimmed = body.trimStart();
|
|
148
|
+
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
85
153
|
function containerEnd(lines: readonly SourceLine[], start: number, indent: number): number | null {
|
|
86
154
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
87
155
|
const body = lines[index]!.body;
|
|
@@ -193,9 +261,24 @@ function locatePath(text: string, parsed: unknown, path: readonly string[]): Loc
|
|
|
193
261
|
if (matches.length > 1) return null;
|
|
194
262
|
prefix.push(path[depth]!);
|
|
195
263
|
if (matches.length === 0) {
|
|
264
|
+
const seen = readPath(parsed, prefix);
|
|
265
|
+
// An empty inline map is the one flow shape we can adopt: rewriting that
|
|
266
|
+
// single line into block form adds our subtree and re-renders nothing the
|
|
267
|
+
// user wrote, because there is nothing in it (#4260).
|
|
268
|
+
const inline: number[] = [];
|
|
269
|
+
const populatedFlow: number[] = [];
|
|
270
|
+
for (let index = rangeStart; index < rangeEnd; index += 1) {
|
|
271
|
+
const body = lines[index]!.body;
|
|
272
|
+
if (isEmptyInlineMapKey(body, indent, path[depth]!)) inline.push(index);
|
|
273
|
+
else if (isPopulatedInlineFlowKey(body, indent, path[depth]!)) populatedFlow.push(index);
|
|
274
|
+
}
|
|
275
|
+
if (inline.length === 1 && isPlainRecord(seen) && Object.keys(seen).length === 0) {
|
|
276
|
+
return { kind: "replace-line", entry: { lines, index: inline[0]!, indent, missingDepth: depth } };
|
|
277
|
+
}
|
|
278
|
+
if (populatedFlow.length === 1 && seen !== undefined) return { kind: "unsupported-style" };
|
|
196
279
|
// The parser saw this key through syntax we do not patch (quoted/flow,
|
|
197
280
|
// merge aliases, or an ambiguous indentation shape).
|
|
198
|
-
if (
|
|
281
|
+
if (seen !== undefined) return null;
|
|
199
282
|
const insertAt = rangeEnd < lines.length ? lines[rangeEnd]!.start : text.length;
|
|
200
283
|
return { kind: "missing", entry: { lines, missingDepth: depth, indent, insertAt } };
|
|
201
284
|
}
|
|
@@ -209,7 +292,20 @@ function locatePath(text: string, parsed: unknown, path: readonly string[]): Loc
|
|
|
209
292
|
if (leafEnd === null) return null;
|
|
210
293
|
return { kind: "existing", entry: { lines, index, indent, endIndex: leafEnd } };
|
|
211
294
|
}
|
|
212
|
-
|
|
295
|
+
const container = readPath(parsed, prefix);
|
|
296
|
+
// `key:` with no children parses as null. The key line matched, so the
|
|
297
|
+
// missing-key branch above never runs, and `isPlainRecord(null)` is false —
|
|
298
|
+
// so an empty container used to refuse the whole document (#4260). Insert
|
|
299
|
+
// our subtree as its first child instead.
|
|
300
|
+
if (container === null) {
|
|
301
|
+
const insertAt = end < lines.length ? lines[end]!.start : text.length;
|
|
302
|
+
return {
|
|
303
|
+
kind: "missing",
|
|
304
|
+
entry: { lines, missingDepth: depth + 1, indent: indent + 2, insertAt },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (!isPlainRecord(container)) return null;
|
|
308
|
+
if (firstChildOpensFlow(lines, index, end, indent)) return { kind: "unsupported-style" };
|
|
213
309
|
rangeStart = index + 1;
|
|
214
310
|
rangeEnd = end;
|
|
215
311
|
parentIndent = indent;
|
|
@@ -241,7 +337,7 @@ function upsertSource(
|
|
|
241
337
|
value: unknown,
|
|
242
338
|
): string | null {
|
|
243
339
|
const located = locatePath(text, parsed, path);
|
|
244
|
-
if (located === null) return null;
|
|
340
|
+
if (located === null || located.kind === "unsupported-style") return null;
|
|
245
341
|
const eol = lineEnding(text);
|
|
246
342
|
if (located.kind === "existing") {
|
|
247
343
|
const { lines, index, indent, endIndex } = located.entry;
|
|
@@ -250,6 +346,13 @@ function upsertSource(
|
|
|
250
346
|
const candidate = `${text.slice(0, startOffset)}${rendered({ [path[path.length - 1]!]: value }, indent, eol)}${text.slice(endOffset)}`;
|
|
251
347
|
return preserveFinalNewline(candidate, text, eol);
|
|
252
348
|
}
|
|
349
|
+
if (located.kind === "replace-line") {
|
|
350
|
+
const { lines, index, indent, missingDepth } = located.entry;
|
|
351
|
+
const startOffset = lines[index]!.start;
|
|
352
|
+
const endOffset = index + 1 < lines.length ? lines[index + 1]!.start : text.length;
|
|
353
|
+
const insertion = rendered(nestedValue(path.slice(missingDepth), value), indent, eol);
|
|
354
|
+
return preserveFinalNewline(`${text.slice(0, startOffset)}${insertion}${text.slice(endOffset)}`, text, eol);
|
|
355
|
+
}
|
|
253
356
|
|
|
254
357
|
const { missingDepth, indent, insertAt } = located.entry;
|
|
255
358
|
const prefix = insertAt > 0 && !text.slice(0, insertAt).endsWith("\n") ? eol : "";
|
|
@@ -338,6 +441,22 @@ export function patchYamlFragmentSource(
|
|
|
338
441
|
return patched !== null && semanticallyMatches(patched, expected) ? patched : null;
|
|
339
442
|
}
|
|
340
443
|
|
|
444
|
+
/**
|
|
445
|
+
* True when a refusal on this path is caused by a flow-style container rather
|
|
446
|
+
* than by comments or formatting we would have to re-render. DSH writes that
|
|
447
|
+
* shape itself, so naming it is the difference between an actionable message
|
|
448
|
+
* and one that sends the user hunting for a comment that is not there (#4260).
|
|
449
|
+
*/
|
|
450
|
+
export function yamlFragmentUnsupportedStyle(text: string, path: readonly string[]): boolean {
|
|
451
|
+
let parsed: unknown;
|
|
452
|
+
try {
|
|
453
|
+
parsed = text.trim().length === 0 ? {} : Bun.YAML.parse(text);
|
|
454
|
+
} catch {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
return locatePath(text, parsed, path)?.kind === "unsupported-style";
|
|
458
|
+
}
|
|
459
|
+
|
|
341
460
|
/** Backward-compatible OMP wrapper around the generic path patcher. */
|
|
342
461
|
export function patchOmpYamlSource(
|
|
343
462
|
text: string,
|
|
@@ -63,6 +63,10 @@ function cloneFragment(fragment: ManagedFragment): ManagedFragment {
|
|
|
63
63
|
export function refreshablePathsOf(
|
|
64
64
|
contribution: ManagedContribution,
|
|
65
65
|
): readonly (readonly string[])[] {
|
|
66
|
+
if (contribution.clientId === "cline") return [
|
|
67
|
+
["settings", "providers", OPENCODE_PROVIDER_ID, "updatedAt"],
|
|
68
|
+
["settings", "providers", OPENCODE_PROVIDER_ID, "settings", "model"],
|
|
69
|
+
];
|
|
66
70
|
if (contribution.clientId !== "zcode") return [];
|
|
67
71
|
const fragment = contribution.fragments.find(candidate => (
|
|
68
72
|
candidate.path.length === 2
|
|
@@ -90,6 +94,9 @@ export function validRefreshablePaths(
|
|
|
90
94
|
contribution: ManagedContribution,
|
|
91
95
|
value: unknown,
|
|
92
96
|
): value is readonly (readonly string[])[] {
|
|
97
|
+
if (contribution.clientId === "cline") {
|
|
98
|
+
return JSON.stringify(value) === JSON.stringify(refreshablePathsOf(contribution));
|
|
99
|
+
}
|
|
93
100
|
if (contribution.clientId !== "zcode" || !Array.isArray(value) || value.length === 0) {
|
|
94
101
|
return false;
|
|
95
102
|
}
|
|
@@ -101,6 +101,42 @@ export interface OwnershipRecord {
|
|
|
101
101
|
opId: string;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Recovery needs proven metadata, unlike the tolerant status-reader fallback. */
|
|
105
|
+
export function isOwnershipRecord(value: unknown): value is OwnershipRecord {
|
|
106
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
107
|
+
const record = value as Record<string, unknown>;
|
|
108
|
+
const hash = (item: unknown) => typeof item === "string" && /^[a-f0-9]{16}$/.test(item);
|
|
109
|
+
const paths = (item: unknown) => Array.isArray(item) && item.every(path =>
|
|
110
|
+
Array.isArray(path) && path.length > 0 && path.every(key => typeof key === "string" && key.length > 0));
|
|
111
|
+
return typeof record.clientId === "string" && record.clientId.length > 0
|
|
112
|
+
&& typeof record.configPath === "string" && record.configPath.length > 0
|
|
113
|
+
&& typeof record.opId === "string" && record.opId.length > 0
|
|
114
|
+
&& typeof record.appliedAt === "string" && Number.isFinite(Date.parse(record.appliedAt))
|
|
115
|
+
&& hash(record.fileFingerprint) && hash(record.blockFingerprint)
|
|
116
|
+
&& paths(record.fragmentPaths) && (record.fragmentPaths as unknown[]).length > 0
|
|
117
|
+
&& ["semanticBlockFingerprint", "protectedBlockFingerprint", "semanticProtectedBlockFingerprint"]
|
|
118
|
+
.every(key => record[key] === undefined || hash(record[key]))
|
|
119
|
+
&& (record.refreshablePaths === undefined || paths(record.refreshablePaths))
|
|
120
|
+
&& (record.createdContainers === undefined || (Array.isArray(record.createdContainers)
|
|
121
|
+
&& record.createdContainers.every(path => typeof path === "string")));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Missing is empty; corrupt/unreadable ownership is uncertainty and must abort recovery. */
|
|
125
|
+
export function readRecordsStrict(dir: string = integrationsDir()): Partial<Record<IntegrationClientId, OwnershipRecord>> {
|
|
126
|
+
let text: string;
|
|
127
|
+
try { text = readFileSync(recordsPath(dir), "utf8"); }
|
|
128
|
+
catch (error) {
|
|
129
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
130
|
+
throw new Error("integration ownership cannot be read for recovery");
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const parsed: unknown = JSON.parse(text);
|
|
134
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)
|
|
135
|
+
|| !Object.entries(parsed).every(([key, value]) => isOwnershipRecord(value) && value.clientId === key)) throw new Error();
|
|
136
|
+
return parsed as Partial<Record<IntegrationClientId, OwnershipRecord>>;
|
|
137
|
+
} catch { throw new Error("integration ownership is invalid for recovery"); }
|
|
138
|
+
}
|
|
139
|
+
|
|
104
140
|
/**
|
|
105
141
|
* The integrations directory itself. Every primitive takes THIS path, never a
|
|
106
142
|
* config root, so a caller cannot accidentally produce
|
|
@@ -12,6 +12,8 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import {
|
|
14
14
|
ClientPathError,
|
|
15
|
+
clineConfigPath,
|
|
16
|
+
clineSettingsDir,
|
|
15
17
|
EXPORT_CLIENTS,
|
|
16
18
|
asideAccountDir,
|
|
17
19
|
asideConfigPath,
|
|
@@ -26,6 +28,8 @@ import {
|
|
|
26
28
|
kimiHomeDir,
|
|
27
29
|
mcodeConfigPath,
|
|
28
30
|
mcodeHomeDir,
|
|
31
|
+
omoAgentDir,
|
|
32
|
+
omoConfigPath,
|
|
29
33
|
ompAgentDir,
|
|
30
34
|
ompModelsConfigPath,
|
|
31
35
|
opencodeGlobalConfigPath,
|
|
@@ -280,6 +284,29 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
280
284
|
*/
|
|
281
285
|
detectDir: (env = process.env, home = homedir()) => raycastAiDir(env, home),
|
|
282
286
|
},
|
|
287
|
+
omo: {
|
|
288
|
+
id: "omo",
|
|
289
|
+
configPath: (env = process.env, home = homedir()) => omoConfigPath(env, home),
|
|
290
|
+
/*
|
|
291
|
+
* The AGENT directory, not `~/.omo`. The v4 launcher wrapper creates
|
|
292
|
+
* `~/.omo` to hold `binary-runtime` without ever creating `agent/`, so
|
|
293
|
+
* detecting on the parent reports an omo v5 install that is not there --
|
|
294
|
+
* and `installed` is what stops apply from writing a catalog for an engine
|
|
295
|
+
* that will never read it. Prime's agent directory and Aside's account
|
|
296
|
+
* directory are the same shape; Pi's parent-directory check is the odd one.
|
|
297
|
+
*
|
|
298
|
+
* No `sourcePreservingYaml` (JSON), no `writerLock` (single writer), and no
|
|
299
|
+
* `resolvePaths` -- unlike Aside, both omo paths are a pure function of env
|
|
300
|
+
* and home, so reading them in sequence cannot straddle a state change.
|
|
301
|
+
*/
|
|
302
|
+
detectDir: (env = process.env, home = homedir()) => omoAgentDir(env, home),
|
|
303
|
+
},
|
|
304
|
+
cline: {
|
|
305
|
+
id: "cline",
|
|
306
|
+
configPath: (env = process.env, home = homedir()) => clineConfigPath(env, home),
|
|
307
|
+
detectDir: (env = process.env, home = homedir()) => clineSettingsDir(env, home),
|
|
308
|
+
writerLock: { suffix: ".lock" },
|
|
309
|
+
},
|
|
283
310
|
};
|
|
284
311
|
|
|
285
312
|
export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Design of record: devlog/_fin/260802_client_toggle_api/021 §3.
|
|
10
10
|
*/
|
|
11
|
+
import { createClineIO } from "./cline-io";
|
|
12
|
+
import { parseClineDocument } from "./cline-document";
|
|
11
13
|
import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
12
14
|
import type { OcxConfig } from "../types";
|
|
13
15
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
@@ -487,7 +489,7 @@ function retentionOf(
|
|
|
487
489
|
export function readIntegrationState(input: IntegrationStateInput): IntegrationStatus {
|
|
488
490
|
const store = input.store ?? createIntegrationStateStore();
|
|
489
491
|
retryPendingPrunesOnce(store);
|
|
490
|
-
|
|
492
|
+
let io = input.io ?? store.io();
|
|
491
493
|
const spec = INTEGRATION_CLIENTS[input.clientId];
|
|
492
494
|
const exportSpec = EXPORT_CLIENTS[input.clientId];
|
|
493
495
|
const retention = retentionOf(input.clientId, store);
|
|
@@ -529,6 +531,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
529
531
|
};
|
|
530
532
|
}
|
|
531
533
|
|
|
534
|
+
if (input.clientId === "cline") io = createClineIO(io, configPath, store);
|
|
532
535
|
const target = loadTarget(io, configPath);
|
|
533
536
|
if (!target.ok) {
|
|
534
537
|
return {
|
|
@@ -541,7 +544,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
541
544
|
};
|
|
542
545
|
}
|
|
543
546
|
|
|
544
|
-
const parsed = parseConfig(target.before, exportSpec.format);
|
|
547
|
+
const parsed = input.clientId === "cline" ? parseClineDocument(target.before) : parseConfig(target.before, exportSpec.format);
|
|
545
548
|
const contribution = exportSpec.buildContribution(exportContextOf(input));
|
|
546
549
|
const record = store.readRecords()[input.clientId] ?? null;
|
|
547
550
|
const { state, reason } = classifyIntegration({
|