@adhdev/daemon-core 0.9.82-rc.8 → 0.9.82-rc.81
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/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +10 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +5074 -1177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5038 -1163
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +60 -0
- package/dist/mesh/mesh-events.d.ts +29 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +119 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/repo-mesh-types.d.ts +39 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/cli-adapters/provider-cli-adapter.ts +91 -3
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +20 -10
- package/src/commands/chat-commands.ts +310 -12
- package/src/commands/cli-manager.ts +101 -0
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2435 -414
- package/src/config/chat-history.ts +9 -7
- package/src/config/mesh-config.ts +244 -1
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +3 -3
- package/src/git/git-status.ts +97 -6
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +11 -0
- package/src/index.ts +31 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +176 -0
- package/src/mesh/coordinator-prompt.ts +27 -7
- package/src/mesh/mesh-active-work.ts +243 -0
- package/src/mesh/mesh-events.ts +398 -46
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +138 -1
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/refine-config.ts +306 -0
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/providers/cli-provider-instance.ts +91 -13
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +43 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
|
@@ -1387,22 +1387,23 @@ function callProviderNativeHistoryRead(
|
|
|
1387
1387
|
agentType: string,
|
|
1388
1388
|
canonicalHistory: ProviderCanonicalHistoryConfig | undefined,
|
|
1389
1389
|
scripts: ProviderNativeHistoryScripts | undefined,
|
|
1390
|
-
historySessionId: string,
|
|
1390
|
+
historySessionId: string | undefined,
|
|
1391
1391
|
workspace?: string,
|
|
1392
1392
|
): ProviderNativeHistoryReadResult | null {
|
|
1393
1393
|
const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'readSession');
|
|
1394
1394
|
if (!fn) return null;
|
|
1395
|
+
const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
|
|
1395
1396
|
const result = fn({
|
|
1396
1397
|
agentType,
|
|
1397
|
-
sessionId:
|
|
1398
|
-
historySessionId,
|
|
1398
|
+
sessionId: normalizedSessionId,
|
|
1399
|
+
historySessionId: normalizedSessionId,
|
|
1399
1400
|
workspace,
|
|
1400
1401
|
format: canonicalHistory?.format,
|
|
1401
1402
|
watchPath: canonicalHistory?.watchPath,
|
|
1402
|
-
args: { sessionId:
|
|
1403
|
+
args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, workspace },
|
|
1403
1404
|
});
|
|
1404
1405
|
if (!result || typeof result !== 'object') return null;
|
|
1405
|
-
const records = normalizeProviderNativeHistoryRecords(agentType,
|
|
1406
|
+
const records = normalizeProviderNativeHistoryRecords(agentType, normalizedSessionId, (result as any).messages || (result as any).records);
|
|
1406
1407
|
if (records.length === 0) return null;
|
|
1407
1408
|
return {
|
|
1408
1409
|
records,
|
|
@@ -1419,7 +1420,8 @@ function buildNativeHistoryReadResult(
|
|
|
1419
1420
|
workspace?: string,
|
|
1420
1421
|
): ProviderNativeHistoryReadResult | null {
|
|
1421
1422
|
const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
|
|
1422
|
-
|
|
1423
|
+
const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
|
|
1424
|
+
if (!canonicalHistory || (!normalizedSessionId && !normalizedWorkspace) || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
|
|
1423
1425
|
return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace);
|
|
1424
1426
|
}
|
|
1425
1427
|
|
|
@@ -1478,7 +1480,7 @@ export function readProviderChatHistory(
|
|
|
1478
1480
|
scripts?: ProviderNativeHistoryScripts;
|
|
1479
1481
|
} = {},
|
|
1480
1482
|
): { messages: HistoryMessage[]; hasMore: boolean; source: 'provider-native' | 'adhdev-mirror' | 'native-unavailable'; sourcePath?: string; sourceMtimeMs?: number } {
|
|
1481
|
-
if (isNativeSourceCanonicalHistory(options.canonicalHistory) && options.historySessionId) {
|
|
1483
|
+
if (isNativeSourceCanonicalHistory(options.canonicalHistory) && (options.historySessionId || options.workspace)) {
|
|
1482
1484
|
const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace);
|
|
1483
1485
|
if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
|
|
1484
1486
|
return {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
|
-
import { randomUUID } from 'crypto';
|
|
11
|
+
import { createHash, randomBytes, randomUUID } from 'crypto';
|
|
12
12
|
import { getConfigDir } from './config.js';
|
|
13
13
|
import type {
|
|
14
14
|
LocalMeshConfig,
|
|
@@ -18,8 +18,11 @@ import type {
|
|
|
18
18
|
RepoMeshNodePolicy,
|
|
19
19
|
RepoMeshNodeCapabilities,
|
|
20
20
|
RepoMeshCoordinatorConfig,
|
|
21
|
+
RepoMeshHostMetadata,
|
|
22
|
+
RepoMeshDaemonRole,
|
|
21
23
|
} from '../repo-mesh-types.js';
|
|
22
24
|
import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
|
|
25
|
+
import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
|
|
23
26
|
|
|
24
27
|
// ─── Persistence ────────────────────────────────
|
|
25
28
|
|
|
@@ -112,6 +115,7 @@ export interface CreateMeshOptions {
|
|
|
112
115
|
defaultBranch?: string;
|
|
113
116
|
policy?: Partial<RepoMeshPolicy>;
|
|
114
117
|
coordinator?: RepoMeshCoordinatorConfig;
|
|
118
|
+
meshHost?: RepoMeshHostMetadata;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
|
|
@@ -133,6 +137,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
|
|
|
133
137
|
defaultBranch: opts.defaultBranch,
|
|
134
138
|
policy: mergeMeshPolicy(undefined, opts.policy),
|
|
135
139
|
coordinator: opts.coordinator || {},
|
|
140
|
+
meshHost: opts.meshHost || createDefaultMeshHostMetadata(),
|
|
136
141
|
nodes: [],
|
|
137
142
|
createdAt: now,
|
|
138
143
|
updatedAt: now,
|
|
@@ -148,6 +153,7 @@ export interface UpdateMeshOptions {
|
|
|
148
153
|
defaultBranch?: string;
|
|
149
154
|
policy?: Partial<RepoMeshPolicy>;
|
|
150
155
|
coordinator?: RepoMeshCoordinatorConfig;
|
|
156
|
+
meshHost?: RepoMeshHostMetadata;
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
|
|
@@ -159,6 +165,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
|
|
|
159
165
|
if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
|
|
160
166
|
if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
|
|
161
167
|
if (opts.coordinator) mesh.coordinator = opts.coordinator;
|
|
168
|
+
if (opts.meshHost) mesh.meshHost = opts.meshHost;
|
|
162
169
|
mesh.updatedAt = new Date().toISOString();
|
|
163
170
|
|
|
164
171
|
saveMeshConfig(config);
|
|
@@ -174,6 +181,240 @@ export function deleteMesh(meshId: string): boolean {
|
|
|
174
181
|
return true;
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
function normalizeManualHostAddress(hostAddress: string): string {
|
|
185
|
+
const normalized = hostAddress.trim().replace(/\/+$/, '');
|
|
186
|
+
if (!normalized) throw new Error('hostAddress required');
|
|
187
|
+
let parsed: URL;
|
|
188
|
+
try {
|
|
189
|
+
parsed = new URL(normalized);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error('hostAddress must be a valid http(s) or ws(s) URL');
|
|
192
|
+
}
|
|
193
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
|
|
194
|
+
throw new Error('hostAddress must use http, https, ws, or wss');
|
|
195
|
+
}
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function tokenIdForManualPairing(token: string): string {
|
|
200
|
+
return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeTokenExpiry(value: unknown): string | undefined {
|
|
204
|
+
if (typeof value !== 'string' || !value.trim()) return undefined;
|
|
205
|
+
const date = new Date(value);
|
|
206
|
+
if (Number.isNaN(date.getTime())) throw new Error('expiresAt must be a valid ISO date');
|
|
207
|
+
return date.toISOString();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function assertPairingTokenValid(pairing: RepoMeshHostMetadata['pairing'], rawToken: string, nowIso: string): { ok: true; tokenId: string } | { ok: false; reason: string; expectedTokenId?: string; presentedTokenId?: string } {
|
|
211
|
+
const token = rawToken.trim();
|
|
212
|
+
if (!token) return { ok: false, reason: 'token required' };
|
|
213
|
+
const presentedTokenId = tokenIdForManualPairing(token);
|
|
214
|
+
const expectedTokenId = pairing?.tokenId;
|
|
215
|
+
if (!expectedTokenId || pairing?.status === 'not_configured' || pairing?.status === 'revoked') {
|
|
216
|
+
return { ok: false, reason: 'host pairing token is not configured', presentedTokenId };
|
|
217
|
+
}
|
|
218
|
+
if (pairing.expiresAt && new Date(pairing.expiresAt).getTime() <= new Date(nowIso).getTime()) {
|
|
219
|
+
return { ok: false, reason: 'host pairing token expired', expectedTokenId, presentedTokenId };
|
|
220
|
+
}
|
|
221
|
+
if (presentedTokenId !== expectedTokenId) {
|
|
222
|
+
return { ok: false, reason: 'invalid pairing token', expectedTokenId, presentedTokenId };
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, tokenId: presentedTokenId };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface ConfigureMeshHostPairingOptions {
|
|
228
|
+
hostAddress: string;
|
|
229
|
+
token: string;
|
|
230
|
+
now?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function configureMeshHostPairing(
|
|
234
|
+
meshId: string,
|
|
235
|
+
opts: ConfigureMeshHostPairingOptions,
|
|
236
|
+
): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; hostAddress: string } | undefined {
|
|
237
|
+
const hostAddress = normalizeManualHostAddress(opts.hostAddress);
|
|
238
|
+
const token = opts.token.trim();
|
|
239
|
+
if (!token) throw new Error('token required');
|
|
240
|
+
|
|
241
|
+
const config = loadMeshConfig();
|
|
242
|
+
const mesh = config.meshes.find(m => m.id === meshId);
|
|
243
|
+
if (!mesh) return undefined;
|
|
244
|
+
|
|
245
|
+
const now = opts.now || new Date().toISOString();
|
|
246
|
+
const previous = mesh.meshHost || createDefaultMeshHostMetadata();
|
|
247
|
+
const meshHost: RepoMeshHostMetadata = {
|
|
248
|
+
...previous,
|
|
249
|
+
role: 'member',
|
|
250
|
+
hostAddress,
|
|
251
|
+
pairing: {
|
|
252
|
+
status: 'pairing',
|
|
253
|
+
tokenId: tokenIdForManualPairing(token),
|
|
254
|
+
lastPairedAt: now,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
mesh.meshHost = meshHost;
|
|
259
|
+
mesh.updatedAt = now;
|
|
260
|
+
saveMeshConfig(config);
|
|
261
|
+
return { mesh, meshHost, hostAddress };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface CreateMeshHostPairingTokenOptions {
|
|
265
|
+
token?: string;
|
|
266
|
+
expiresAt?: string;
|
|
267
|
+
now?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createMeshHostPairingToken(
|
|
271
|
+
meshId: string,
|
|
272
|
+
opts: CreateMeshHostPairingTokenOptions = {},
|
|
273
|
+
): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; token: string; tokenId: string; expiresAt?: string } | undefined {
|
|
274
|
+
const config = loadMeshConfig();
|
|
275
|
+
const mesh = config.meshes.find(m => m.id === meshId);
|
|
276
|
+
if (!mesh) return undefined;
|
|
277
|
+
const now = opts.now || new Date().toISOString();
|
|
278
|
+
const token = (opts.token || `mhj_${randomBytes(24).toString('base64url')}`).trim();
|
|
279
|
+
if (!token) throw new Error('token required');
|
|
280
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
281
|
+
const expiresAt = normalizeTokenExpiry(opts.expiresAt);
|
|
282
|
+
const previous = mesh.meshHost || createDefaultMeshHostMetadata();
|
|
283
|
+
if (previous.role === 'member') {
|
|
284
|
+
throw new Error('Mesh Host daemon required to create host pairing tokens; member daemons cannot mint host join tokens.');
|
|
285
|
+
}
|
|
286
|
+
const meshHost: RepoMeshHostMetadata = {
|
|
287
|
+
...previous,
|
|
288
|
+
role: 'host',
|
|
289
|
+
pairing: {
|
|
290
|
+
status: 'pairing',
|
|
291
|
+
tokenId,
|
|
292
|
+
lastPairedAt: now,
|
|
293
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
mesh.meshHost = meshHost;
|
|
297
|
+
mesh.updatedAt = now;
|
|
298
|
+
saveMeshConfig(config);
|
|
299
|
+
return { mesh, meshHost, token, tokenId, ...(expiresAt ? { expiresAt } : {}) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface MeshHostJoinMemberNodeInput {
|
|
303
|
+
id?: string;
|
|
304
|
+
workspace: string;
|
|
305
|
+
repoRoot?: string;
|
|
306
|
+
daemonId?: string;
|
|
307
|
+
machineId?: string;
|
|
308
|
+
userOverrides?: Partial<RepoMeshNodeCapabilities>;
|
|
309
|
+
policy?: RepoMeshNodePolicy;
|
|
310
|
+
role?: RepoMeshDaemonRole;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface ApplyMeshHostJoinOptions {
|
|
314
|
+
token: string;
|
|
315
|
+
memberNode: MeshHostJoinMemberNodeInput;
|
|
316
|
+
memberMeshId?: string;
|
|
317
|
+
now?: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function applyMeshHostJoinRequest(
|
|
321
|
+
meshId: string,
|
|
322
|
+
opts: ApplyMeshHostJoinOptions,
|
|
323
|
+
): { accepted: true; mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; node: LocalMeshNodeEntry; tokenId: string } | { accepted: false; mesh?: LocalMeshEntry; meshHost?: RepoMeshHostMetadata; tokenId?: string; reason: string } | undefined {
|
|
324
|
+
const config = loadMeshConfig();
|
|
325
|
+
const mesh = config.meshes.find(m => m.id === meshId);
|
|
326
|
+
if (!mesh) return undefined;
|
|
327
|
+
const now = opts.now || new Date().toISOString();
|
|
328
|
+
const previous = mesh.meshHost || createDefaultMeshHostMetadata();
|
|
329
|
+
if (previous.role === 'member') {
|
|
330
|
+
return { accepted: false, mesh, meshHost: previous, reason: 'Mesh Host daemon required to accept join requests' };
|
|
331
|
+
}
|
|
332
|
+
const meshHost: RepoMeshHostMetadata = { ...previous, role: 'host' };
|
|
333
|
+
const validation = assertPairingTokenValid(meshHost.pairing, opts.token, now);
|
|
334
|
+
if (!validation.ok) {
|
|
335
|
+
mesh.meshHost = {
|
|
336
|
+
...meshHost,
|
|
337
|
+
pairing: {
|
|
338
|
+
...(meshHost.pairing || { status: 'not_configured' as const }),
|
|
339
|
+
status: 'rejected',
|
|
340
|
+
lastRejectedAt: now,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
mesh.updatedAt = now;
|
|
344
|
+
saveMeshConfig(config);
|
|
345
|
+
return { accepted: false, mesh, meshHost: mesh.meshHost, tokenId: validation.presentedTokenId, reason: validation.reason };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const workspace = opts.memberNode.workspace.trim();
|
|
349
|
+
if (!workspace) throw new Error('memberNode.workspace required');
|
|
350
|
+
const memberId = opts.memberNode.id?.trim();
|
|
351
|
+
let node = mesh.nodes.find(n => (memberId && n.id === memberId) || n.workspace === workspace);
|
|
352
|
+
if (node) {
|
|
353
|
+
node.workspace = workspace;
|
|
354
|
+
node.repoRoot = opts.memberNode.repoRoot;
|
|
355
|
+
node.daemonId = opts.memberNode.daemonId;
|
|
356
|
+
node.machineId = opts.memberNode.machineId;
|
|
357
|
+
node.userOverrides = opts.memberNode.userOverrides || node.userOverrides || {};
|
|
358
|
+
node.policy = { ...(node.policy || {}), ...(opts.memberNode.policy || {}) };
|
|
359
|
+
node.role = 'member';
|
|
360
|
+
} else {
|
|
361
|
+
if (mesh.nodes.length >= 10) throw new Error('Maximum 10 nodes per mesh');
|
|
362
|
+
node = {
|
|
363
|
+
id: memberId || `node_${randomUUID().replace(/-/g, '')}`,
|
|
364
|
+
workspace,
|
|
365
|
+
repoRoot: opts.memberNode.repoRoot,
|
|
366
|
+
daemonId: opts.memberNode.daemonId,
|
|
367
|
+
machineId: opts.memberNode.machineId,
|
|
368
|
+
userOverrides: opts.memberNode.userOverrides || {},
|
|
369
|
+
policy: opts.memberNode.policy || {},
|
|
370
|
+
role: 'member',
|
|
371
|
+
};
|
|
372
|
+
mesh.nodes.push(node);
|
|
373
|
+
}
|
|
374
|
+
mesh.meshHost = {
|
|
375
|
+
...meshHost,
|
|
376
|
+
pairing: {
|
|
377
|
+
...(meshHost.pairing || {}),
|
|
378
|
+
status: 'paired',
|
|
379
|
+
tokenId: validation.tokenId,
|
|
380
|
+
joinedAt: now,
|
|
381
|
+
lastPairedAt: meshHost.pairing?.lastPairedAt || now,
|
|
382
|
+
...(meshHost.pairing?.expiresAt ? { expiresAt: meshHost.pairing.expiresAt } : {}),
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
mesh.updatedAt = now;
|
|
386
|
+
saveMeshConfig(config);
|
|
387
|
+
return { accepted: true, mesh, meshHost: mesh.meshHost, node, tokenId: validation.tokenId };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function markMeshHostPairingJoined(
|
|
391
|
+
meshId: string,
|
|
392
|
+
opts: { hostDaemonId?: string; hostNodeId?: string; joinedAt?: string; token?: string; tokenId?: string },
|
|
393
|
+
): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata } | undefined {
|
|
394
|
+
const config = loadMeshConfig();
|
|
395
|
+
const mesh = config.meshes.find(m => m.id === meshId);
|
|
396
|
+
if (!mesh) return undefined;
|
|
397
|
+
const now = opts.joinedAt || new Date().toISOString();
|
|
398
|
+
const previous = mesh.meshHost || createDefaultMeshHostMetadata();
|
|
399
|
+
const tokenId = opts.tokenId || (opts.token ? tokenIdForManualPairing(opts.token) : previous.pairing?.tokenId);
|
|
400
|
+
mesh.meshHost = {
|
|
401
|
+
...previous,
|
|
402
|
+
role: 'member',
|
|
403
|
+
...(opts.hostDaemonId ? { hostDaemonId: opts.hostDaemonId } : {}),
|
|
404
|
+
...(opts.hostNodeId ? { hostNodeId: opts.hostNodeId } : {}),
|
|
405
|
+
pairing: {
|
|
406
|
+
...(previous.pairing || {}),
|
|
407
|
+
status: 'paired',
|
|
408
|
+
...(tokenId ? { tokenId } : {}),
|
|
409
|
+
joinedAt: now,
|
|
410
|
+
lastPairedAt: previous.pairing?.lastPairedAt || now,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
mesh.updatedAt = now;
|
|
414
|
+
saveMeshConfig(config);
|
|
415
|
+
return { mesh, meshHost: mesh.meshHost };
|
|
416
|
+
}
|
|
417
|
+
|
|
177
418
|
// ─── Node Operations ────────────────────────────
|
|
178
419
|
|
|
179
420
|
export interface AddNodeOptions {
|
|
@@ -186,6 +427,7 @@ export interface AddNodeOptions {
|
|
|
186
427
|
isLocalWorktree?: boolean;
|
|
187
428
|
worktreeBranch?: string;
|
|
188
429
|
clonedFromNodeId?: string;
|
|
430
|
+
role?: RepoMeshDaemonRole;
|
|
189
431
|
}
|
|
190
432
|
|
|
191
433
|
export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
|
|
@@ -213,6 +455,7 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
|
|
|
213
455
|
isLocalWorktree: opts.isLocalWorktree,
|
|
214
456
|
worktreeBranch: opts.worktreeBranch,
|
|
215
457
|
clonedFromNodeId: opts.clonedFromNodeId,
|
|
458
|
+
role: opts.role,
|
|
216
459
|
};
|
|
217
460
|
|
|
218
461
|
mesh.nodes.push(node);
|
|
@@ -802,7 +802,16 @@ export async function handleCliSend(ctx: DevServerContext, req: http.IncomingMes
|
|
|
802
802
|
}
|
|
803
803
|
|
|
804
804
|
try {
|
|
805
|
-
|
|
805
|
+
if (target.category === 'cli') {
|
|
806
|
+
const bundle = getCliTargetBundle(ctx, type, instanceId);
|
|
807
|
+
if (!bundle) {
|
|
808
|
+
ctx.json(res, 404, { error: `No running CLI adapter found for: ${type || instanceId}` });
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
await bundle.adapter.sendMessage(text);
|
|
812
|
+
} else {
|
|
813
|
+
ctx.instanceManager!.sendEvent(target.instanceId, 'send_message', { text });
|
|
814
|
+
}
|
|
806
815
|
ctx.json(res, 200, { sent: true, type: target.type, instanceId: target.instanceId });
|
|
807
816
|
} catch (e: any) {
|
|
808
817
|
ctx.json(res, 500, { error: `Send failed: ${e.message}` });
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* Migrated from @adhdev/core — this is now the single source of truth.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { exec } from 'child_process';
|
|
11
|
+
import { promisify } from 'util';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
import { existsSync, statSync } from 'fs';
|
|
12
14
|
import { platform, homedir } from 'os';
|
|
13
15
|
import * as path from 'path';
|
|
14
16
|
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
@@ -73,25 +75,33 @@ function findCliCommand(command: string): string | null {
|
|
|
73
75
|
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
|
|
74
76
|
return existsSync(resolved) ? resolved : null;
|
|
75
77
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
const isWin = platform() === 'win32';
|
|
79
|
+
const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
|
|
80
|
+
const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
81
|
+
for (const p of paths) {
|
|
82
|
+
if (!p) continue;
|
|
83
|
+
for (const ext of exes) {
|
|
84
|
+
const fullPath = path.join(p, trimmed + ext);
|
|
85
|
+
try {
|
|
86
|
+
if (existsSync(fullPath)) {
|
|
87
|
+
const stat = statSync(fullPath);
|
|
88
|
+
if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
|
|
89
|
+
return fullPath;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch { }
|
|
93
|
+
}
|
|
84
94
|
}
|
|
95
|
+
return null;
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
function getIdeVersion(cliCommand: string): string | null {
|
|
98
|
+
async function getIdeVersion(cliCommand: string): Promise<string | null> {
|
|
88
99
|
try {
|
|
89
|
-
const
|
|
100
|
+
const { stdout } = await execAsync(`"${cliCommand}" --version`, {
|
|
90
101
|
encoding: 'utf-8',
|
|
91
102
|
timeout: 10000,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return result.split('\n')[0] || null;
|
|
103
|
+
});
|
|
104
|
+
return stdout.trim().split('\n')[0] || null;
|
|
95
105
|
} catch {
|
|
96
106
|
return null;
|
|
97
107
|
}
|
|
@@ -152,7 +162,7 @@ export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEIn
|
|
|
152
162
|
const installed = os === 'darwin'
|
|
153
163
|
? !!(resolvedCli || appPath)
|
|
154
164
|
: !!resolvedCli;
|
|
155
|
-
const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
|
|
165
|
+
const version = resolvedCli ? await getIdeVersion(resolvedCli) : null;
|
|
156
166
|
|
|
157
167
|
results.push({
|
|
158
168
|
id: def.id,
|
package/src/git/git-commands.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface GitPushResult extends GitRepoIdentity {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export interface GitCommandServices {
|
|
65
|
-
getStatus?: (params: { workspace: string }) => Promise<GitRepoStatus> | GitRepoStatus;
|
|
65
|
+
getStatus?: (params: { workspace: string; refreshUpstream?: boolean }) => Promise<GitRepoStatus> | GitRepoStatus;
|
|
66
66
|
getDiffSummary?: (params: { workspace: string; staged?: boolean }) => Promise<GitDiffSummary> | GitDiffSummary;
|
|
67
67
|
getDiffFile?: (params: { workspace: string; path: string; staged?: boolean }) => Promise<GitFileDiff> | GitFileDiff;
|
|
68
68
|
createSnapshot?: (params: {
|
|
@@ -171,7 +171,7 @@ const defaultSnapshotStore = createGitSnapshotStore({
|
|
|
171
171
|
|
|
172
172
|
export function createDefaultGitCommandServices(): GitCommandServices {
|
|
173
173
|
return {
|
|
174
|
-
getStatus: ({ workspace }) => getGitRepoStatus(workspace),
|
|
174
|
+
getStatus: ({ workspace, refreshUpstream }) => getGitRepoStatus(workspace, { refreshUpstream }),
|
|
175
175
|
getDiffSummary: ({ workspace }) => getGitDiffSummary(workspace),
|
|
176
176
|
getDiffFile: ({ workspace, path: filePath }) => getGitFileDiff(workspace, filePath),
|
|
177
177
|
createSnapshot: ({ workspace, reason, sessionId, turnId }) => defaultSnapshotStore.create({
|
|
@@ -290,7 +290,7 @@ export async function handleGitCommand(
|
|
|
290
290
|
switch (command) {
|
|
291
291
|
case 'git_status': {
|
|
292
292
|
if (!services.getStatus) return serviceNotImplemented(command);
|
|
293
|
-
const status = await runService(() => services.getStatus!({ workspace }));
|
|
293
|
+
const status = await runService(() => services.getStatus!({ workspace, refreshUpstream: optionalBoolean(args?.refreshUpstream) }));
|
|
294
294
|
return 'success' in status ? status : { success: true, status };
|
|
295
295
|
}
|
|
296
296
|
|
package/src/git/git-status.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import type { GitRepoStatus, GitSubmoduleStatus } from './git-types.js';
|
|
1
|
+
import type { GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
|
|
2
2
|
import { GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
|
|
3
3
|
|
|
4
|
+
type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo: boolean };
|
|
5
|
+
|
|
4
6
|
export interface GitStatusOptions {
|
|
5
7
|
timeoutMs?: number;
|
|
6
8
|
/** When true, include submodule status in the result. Defaults to true. */
|
|
7
9
|
includeSubmodules?: boolean;
|
|
8
10
|
/** Optional filter to exclude specific submodule paths from status */
|
|
9
11
|
submoduleIgnorePaths?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* When true, refresh the tracked remote before trusting ahead/behind.
|
|
14
|
+
* Callers should opt into this only for convergence-critical surfaces.
|
|
15
|
+
*/
|
|
16
|
+
refreshUpstream?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface GitUpstreamProbe {
|
|
20
|
+
upstreamStatus: GitUpstreamFreshness;
|
|
21
|
+
upstreamFetchedAt?: number;
|
|
22
|
+
upstreamFetchError?: string;
|
|
10
23
|
}
|
|
11
24
|
|
|
12
25
|
export async function getGitRepoStatus(
|
|
@@ -18,8 +31,16 @@ export async function getGitRepoStatus(
|
|
|
18
31
|
|
|
19
32
|
try {
|
|
20
33
|
const repo = await resolveGitRepository(workspace, options);
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
let parsed = await readPorcelainStatus(repo, options);
|
|
35
|
+
let upstreamProbe: GitUpstreamProbe = getInitialUpstreamProbe(parsed);
|
|
36
|
+
|
|
37
|
+
if (options.refreshUpstream) {
|
|
38
|
+
upstreamProbe = await refreshTrackedUpstream(repo, parsed, options);
|
|
39
|
+
if (upstreamProbe.upstreamStatus === 'fresh') {
|
|
40
|
+
parsed = await readPorcelainStatus(repo, options);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
const head = await readHead(repo, options);
|
|
24
45
|
const stashCount = await readStashCount(repo, options);
|
|
25
46
|
|
|
@@ -36,6 +57,9 @@ export async function getGitRepoStatus(
|
|
|
36
57
|
headCommit: head.commit,
|
|
37
58
|
headMessage: head.message,
|
|
38
59
|
upstream: parsed.upstream,
|
|
60
|
+
upstreamStatus: parsed.upstream ? upstreamProbe.upstreamStatus : 'no_upstream',
|
|
61
|
+
upstreamFetchedAt: upstreamProbe.upstreamFetchedAt,
|
|
62
|
+
upstreamFetchError: upstreamProbe.upstreamFetchError,
|
|
39
63
|
ahead: parsed.ahead,
|
|
40
64
|
behind: parsed.behind,
|
|
41
65
|
staged: parsed.staged,
|
|
@@ -74,6 +98,72 @@ interface ParsedPorcelainStatus {
|
|
|
74
98
|
conflictFiles: string[];
|
|
75
99
|
}
|
|
76
100
|
|
|
101
|
+
async function readPorcelainStatus(repo: ResolvedGitRepo, options: GitStatusOptions): Promise<ParsedPorcelainStatus> {
|
|
102
|
+
const statusOutput = await runGit(repo, ['status', '--porcelain=v2', '--branch'], options);
|
|
103
|
+
return parsePorcelainV2Status(statusOutput.stdout);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getInitialUpstreamProbe(parsed: ParsedPorcelainStatus): GitUpstreamProbe {
|
|
107
|
+
return {
|
|
108
|
+
upstreamStatus: parsed.upstream ? 'unchecked' : 'no_upstream',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function refreshTrackedUpstream(
|
|
113
|
+
repo: ResolvedGitRepo,
|
|
114
|
+
parsed: ParsedPorcelainStatus,
|
|
115
|
+
options: GitStatusOptions,
|
|
116
|
+
): Promise<GitUpstreamProbe> {
|
|
117
|
+
if (!parsed.upstream || !parsed.branch) {
|
|
118
|
+
return { upstreamStatus: 'no_upstream' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const remoteName = (await readBranchRemote(repo, parsed.branch, options)) ?? inferRemoteName(parsed.upstream);
|
|
122
|
+
if (!remoteName) {
|
|
123
|
+
return {
|
|
124
|
+
upstreamStatus: 'stale',
|
|
125
|
+
upstreamFetchError: `Unable to resolve remote for upstream '${parsed.upstream}'`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await runGit(repo, ['fetch', '--quiet', '--prune', '--no-tags', remoteName], options);
|
|
131
|
+
return {
|
|
132
|
+
upstreamStatus: 'fresh',
|
|
133
|
+
upstreamFetchedAt: Date.now(),
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
upstreamStatus: 'stale',
|
|
138
|
+
upstreamFetchError: formatGitError(error),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readBranchRemote(repo: ResolvedGitRepo, branch: string, options: GitStatusOptions): Promise<string | null> {
|
|
144
|
+
try {
|
|
145
|
+
const result = await runGit(repo, ['config', '--get', `branch.${branch}.remote`], options);
|
|
146
|
+
return result.stdout.trim() || null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function inferRemoteName(upstream: string): string | null {
|
|
153
|
+
const [remoteName] = upstream.split('/');
|
|
154
|
+
return remoteName?.trim() || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatGitError(error: unknown): string {
|
|
158
|
+
if (error instanceof GitCommandError) {
|
|
159
|
+
return error.stderr || error.message;
|
|
160
|
+
}
|
|
161
|
+
if (error instanceof Error) {
|
|
162
|
+
return error.message;
|
|
163
|
+
}
|
|
164
|
+
return String(error);
|
|
165
|
+
}
|
|
166
|
+
|
|
77
167
|
export function parsePorcelainV2Status(output: string): ParsedPorcelainStatus {
|
|
78
168
|
const parsed: ParsedPorcelainStatus = {
|
|
79
169
|
branch: null,
|
|
@@ -145,7 +235,7 @@ export function parsePorcelainV2Status(output: string): ParsedPorcelainStatus {
|
|
|
145
235
|
}
|
|
146
236
|
|
|
147
237
|
async function readHead(
|
|
148
|
-
repo:
|
|
238
|
+
repo: ResolvedGitRepo,
|
|
149
239
|
options: GitStatusOptions,
|
|
150
240
|
): Promise<{ commit: string | null; message: string | null }> {
|
|
151
241
|
try {
|
|
@@ -163,7 +253,7 @@ async function readHead(
|
|
|
163
253
|
}
|
|
164
254
|
|
|
165
255
|
async function readStashCount(
|
|
166
|
-
repo:
|
|
256
|
+
repo: ResolvedGitRepo,
|
|
167
257
|
options: GitStatusOptions,
|
|
168
258
|
): Promise<number> {
|
|
169
259
|
try {
|
|
@@ -187,6 +277,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
|
|
|
187
277
|
headCommit: null,
|
|
188
278
|
headMessage: null,
|
|
189
279
|
upstream: null,
|
|
280
|
+
upstreamStatus: 'unavailable',
|
|
190
281
|
ahead: 0,
|
|
191
282
|
behind: 0,
|
|
192
283
|
staged: 0,
|
|
@@ -206,7 +297,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
|
|
|
206
297
|
// ─── Submodule Status ───────────────────────────
|
|
207
298
|
|
|
208
299
|
async function getSubmoduleStatuses(
|
|
209
|
-
repo:
|
|
300
|
+
repo: ResolvedGitRepo,
|
|
210
301
|
options: GitStatusOptions,
|
|
211
302
|
): Promise<GitSubmoduleStatus[]> {
|
|
212
303
|
if (!repo.repoRoot) return [];
|
package/src/git/git-summary.ts
CHANGED
|
@@ -22,6 +22,9 @@ export function createGitCompactSummary(status: GitRepoStatus, diffSummary?: Git
|
|
|
22
22
|
isGitRepo: status.isGitRepo,
|
|
23
23
|
repoRoot: status.repoRoot,
|
|
24
24
|
branch: status.branch,
|
|
25
|
+
upstreamStatus: status.upstreamStatus,
|
|
26
|
+
upstreamFetchedAt: status.upstreamFetchedAt,
|
|
27
|
+
upstreamFetchError: status.upstreamFetchError,
|
|
25
28
|
dirty:
|
|
26
29
|
status.staged > 0 ||
|
|
27
30
|
status.modified > 0 ||
|
package/src/git/git-types.ts
CHANGED
|
@@ -40,11 +40,19 @@ export interface GitSubmoduleStatus {
|
|
|
40
40
|
error?: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export type GitUpstreamFreshness = 'fresh' | 'unchecked' | 'stale' | 'no_upstream' | 'unavailable';
|
|
44
|
+
|
|
43
45
|
export interface GitRepoStatus extends GitRepoIdentity {
|
|
44
46
|
branch: string | null;
|
|
45
47
|
headCommit: string | null;
|
|
46
48
|
headMessage: string | null;
|
|
47
49
|
upstream: string | null;
|
|
50
|
+
/** Whether ahead/behind was verified against a freshly fetched upstream ref. */
|
|
51
|
+
upstreamStatus: GitUpstreamFreshness;
|
|
52
|
+
/** Timestamp for the fetch that refreshed upstream refs when upstreamStatus === 'fresh'. */
|
|
53
|
+
upstreamFetchedAt?: number;
|
|
54
|
+
/** Error from the last refresh attempt when upstreamStatus === 'stale'. */
|
|
55
|
+
upstreamFetchError?: string;
|
|
48
56
|
ahead: number;
|
|
49
57
|
behind: number;
|
|
50
58
|
staged: number;
|
|
@@ -134,6 +142,9 @@ export interface GitCompactSummary {
|
|
|
134
142
|
isGitRepo: boolean;
|
|
135
143
|
repoRoot: string | null;
|
|
136
144
|
branch: string | null;
|
|
145
|
+
upstreamStatus: GitUpstreamFreshness;
|
|
146
|
+
upstreamFetchedAt?: number;
|
|
147
|
+
upstreamFetchError?: string;
|
|
137
148
|
dirty: boolean;
|
|
138
149
|
changedFiles: number;
|
|
139
150
|
ahead: number;
|