@adhdev/daemon-core 0.9.62 → 0.9.64

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.62",
3
+ "version": "0.9.64",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.62",
3
+ "version": "0.9.64",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -344,6 +344,36 @@ function findLastMessageIndexBySignature(messages: ChatMessage[], signature: str
344
344
  return -1;
345
345
  }
346
346
 
347
+ function isReadChatConversationAnchorMessage(message: ChatMessage | null | undefined): boolean {
348
+ if (!message) return false;
349
+ const role = String(message.role || '').trim().toLowerCase();
350
+ if (role !== 'user' && role !== 'assistant') return false;
351
+ const kind = String(message.kind || 'standard').trim().toLowerCase();
352
+ return !kind || kind === 'standard';
353
+ }
354
+
355
+ function buildVisibleReadChatTailMessages(messages: ChatMessage[], tailLimit: number): ChatMessage[] {
356
+ const totalMessages = messages.length;
357
+ if (tailLimit <= 0 || totalMessages <= tailLimit) return messages;
358
+
359
+ const tailMessages = messages.slice(-tailLimit);
360
+ if (tailMessages.some(isReadChatConversationAnchorMessage)) return tailMessages;
361
+
362
+ const hiddenMessages = messages.slice(0, totalMessages - tailLimit);
363
+ const anchors: ChatMessage[] = [];
364
+ const seenRoles = new Set<string>();
365
+ for (let index = hiddenMessages.length - 1; index >= 0 && anchors.length < 2; index -= 1) {
366
+ const message = hiddenMessages[index];
367
+ if (!isReadChatConversationAnchorMessage(message)) continue;
368
+ const role = String(message.role || '').trim().toLowerCase();
369
+ if (seenRoles.has(role)) continue;
370
+ seenRoles.add(role);
371
+ anchors.unshift(message);
372
+ }
373
+
374
+ return anchors.length > 0 ? [...anchors, ...tailMessages] : tailMessages;
375
+ }
376
+
347
377
  function buildBoundedTailSync(messages: ChatMessage[], cursor: Required<ReadChatCursor>): {
348
378
  syncMode: ReadChatSyncMode;
349
379
  replaceFrom: number;
@@ -352,9 +382,7 @@ function buildBoundedTailSync(messages: ChatMessage[], cursor: Required<ReadChat
352
382
  lastMessageSignature: string;
353
383
  } {
354
384
  const totalMessages = messages.length;
355
- const tailMessages = cursor.tailLimit > 0 && totalMessages > cursor.tailLimit
356
- ? messages.slice(-cursor.tailLimit)
357
- : messages;
385
+ const tailMessages = buildVisibleReadChatTailMessages(messages, cursor.tailLimit);
358
386
  return {
359
387
  syncMode: 'full',
360
388
  replaceFrom: 0,
@@ -495,8 +523,8 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
495
523
  const messages = collapseReplayDuplicatesFromReadChat(normalizeReadChatMessages(validatedPayload));
496
524
  const cursor = normalizeReadChatCursor(args);
497
525
  if (!cursor.knownMessageCount && !cursor.lastMessageSignature && cursor.tailLimit > 0 && messages.length > cursor.tailLimit) {
498
- const tailMessages = messages.slice(-cursor.tailLimit);
499
- const lastMessageSignature = getChatMessageSignature(tailMessages[tailMessages.length - 1]);
526
+ const tailMessages = buildVisibleReadChatTailMessages(messages, cursor.tailLimit);
527
+ const lastMessageSignature = getChatMessageSignature(messages[messages.length - 1]);
500
528
  return {
501
529
  success: true,
502
530
  ...validatedPayload,
@@ -53,6 +53,14 @@ export interface GitStashPushResult extends GitRepoIdentity {
53
53
  lastCheckedAt: number;
54
54
  }
55
55
 
56
+ export interface GitPushResult extends GitRepoIdentity {
57
+ remote: string;
58
+ branch: string;
59
+ output: string;
60
+ newBranch: boolean;
61
+ lastCheckedAt: number;
62
+ }
63
+
56
64
  export interface GitCommandServices {
57
65
  getStatus?: (params: { workspace: string }) => Promise<GitRepoStatus> | GitRepoStatus;
58
66
  getDiffSummary?: (params: { workspace: string; staged?: boolean }) => Promise<GitDiffSummary> | GitDiffSummary;
@@ -88,6 +96,7 @@ export interface GitCommandServices {
88
96
  stashPop?: (params: { workspace: string; stashRef?: string }) => Promise<void>;
89
97
  checkoutFiles?: (params: { workspace: string; paths: string[] }) => Promise<{ checkedOut: string[] }>;
90
98
  getRemoteUrl?: (params: { workspace: string; remote?: string }) => Promise<{ remoteUrl: string; remote: string }>;
99
+ push?: (params: { workspace: string; remote?: string; branch?: string; setUpstream?: boolean }) => Promise<GitPushResult>;
91
100
  }
92
101
 
93
102
  type GitCommandFailure = {
@@ -107,7 +116,8 @@ type GitCommandSuccess =
107
116
  | { success: true; stash: GitStashPushResult }
108
117
  | { success: true; stashPopped: true }
109
118
  | { success: true; checkedOut: string[] }
110
- | { success: true; remoteUrl: string; remote: string };
119
+ | { success: true; remoteUrl: string; remote: string }
120
+ | { success: true; push: GitPushResult };
111
121
 
112
122
  export type GitCommandResult = GitCommandSuccess | GitCommandFailure;
113
123
 
@@ -123,6 +133,7 @@ const GIT_COMMAND_NAMES = new Set<GitCommandName>([
123
133
  'git_stash_pop',
124
134
  'git_checkout_files',
125
135
  'git_remote_url',
136
+ 'git_push',
126
137
  ]);
127
138
 
128
139
  const SNAPSHOT_REASONS = new Set<GitSnapshotReason>([
@@ -175,6 +186,8 @@ export function createDefaultGitCommandServices(): GitCommandServices {
175
186
  stashPop: async ({ workspace, stashRef }) => gitStashPop(workspace, stashRef),
176
187
  checkoutFiles: async ({ workspace, paths }) => gitCheckoutFiles(workspace, paths),
177
188
  getRemoteUrl: async ({ workspace, remote = 'origin' }) => gitGetRemoteUrl(workspace, remote),
189
+ push: async ({ workspace, remote = 'origin', branch, setUpstream = false }) =>
190
+ gitPush(workspace, remote, branch, setUpstream),
178
191
  };
179
192
  }
180
193
 
@@ -385,6 +398,21 @@ export async function handleGitCommand(
385
398
  return { success: true, remoteUrl: remoteResult.remoteUrl, remote: remoteResult.remote };
386
399
  }
387
400
 
401
+ case 'git_push': {
402
+ if (!services.push) return serviceNotImplemented(command);
403
+ const remote = typeof args?.remote === 'string' && args.remote.trim() ? args.remote.trim() : 'origin';
404
+ const branch = typeof args?.branch === 'string' && args.branch.trim() ? args.branch.trim() : undefined;
405
+ const setUpstream = Boolean(args?.setUpstream);
406
+ if (!/^[a-zA-Z0-9_.\-]+$/.test(remote)) {
407
+ return failure('invalid_args', 'remote must contain only alphanumeric characters, dots, hyphens, and underscores');
408
+ }
409
+ if (branch !== undefined && !/^[a-zA-Z0-9/_.\-]+$/.test(branch)) {
410
+ return failure('invalid_args', 'branch must contain only alphanumeric characters, slashes, dots, hyphens, and underscores');
411
+ }
412
+ const pushResult = await runService(() => services.push!({ workspace, remote, branch, setUpstream }));
413
+ return 'success' in pushResult ? pushResult : { success: true, push: pushResult };
414
+ }
415
+
388
416
  default:
389
417
  return failure('invalid_args', `Unknown Git command: ${command}`);
390
418
  }
@@ -512,6 +540,61 @@ async function gitGetRemoteUrl(workspace: string, remote: string): Promise<{ rem
512
540
  return { remoteUrl, remote };
513
541
  }
514
542
 
543
+ async function gitPush(
544
+ workspace: string,
545
+ remote: string,
546
+ branch: string | undefined,
547
+ setUpstream: boolean,
548
+ ): Promise<GitPushResult> {
549
+ const lastCheckedAt = Date.now();
550
+ const repo = await resolveGitRepository(workspace);
551
+ const repoRoot = repo.repoRoot!;
552
+
553
+ // Resolve branch name if not provided
554
+ let resolvedBranch = branch;
555
+ if (!resolvedBranch) {
556
+ const branchResult = await runGit(repo, ['symbolic-ref', '--short', 'HEAD'], { cwd: repoRoot });
557
+ resolvedBranch = branchResult.stdout.trim();
558
+ if (!resolvedBranch) {
559
+ throw new GitCommandError('git_command_failed', 'Cannot push: not on a branch (detached HEAD)');
560
+ }
561
+ }
562
+
563
+ const pushArgs = ['push'];
564
+ if (setUpstream) pushArgs.push('--set-upstream');
565
+ pushArgs.push(remote, resolvedBranch);
566
+
567
+ let output = '';
568
+ let newBranch = false;
569
+ try {
570
+ const result = await runGit(repo, pushArgs, { cwd: repoRoot });
571
+ output = (result.stdout + result.stderr).trim();
572
+ newBranch = /\[new branch\]/i.test(output);
573
+ } catch (err: any) {
574
+ const errOutput = (err?.stdout ?? '') + (err?.stderr ?? '');
575
+ // --set-upstream hint: retry with --set-upstream automatically
576
+ if (!setUpstream && /no upstream branch|set-upstream/i.test(errOutput)) {
577
+ const retryArgs = ['push', '--set-upstream', remote, resolvedBranch];
578
+ const retryResult = await runGit(repo, retryArgs, { cwd: repoRoot });
579
+ output = (retryResult.stdout + retryResult.stderr).trim();
580
+ newBranch = /\[new branch\]/i.test(output);
581
+ } else {
582
+ throw new GitCommandError('git_command_failed', errOutput || err?.message || 'git push failed');
583
+ }
584
+ }
585
+
586
+ return {
587
+ workspace: repo.workspace,
588
+ repoRoot,
589
+ isGitRepo: true,
590
+ remote,
591
+ branch: resolvedBranch,
592
+ output,
593
+ newBranch,
594
+ lastCheckedAt,
595
+ };
596
+ }
597
+
515
598
  function formatOptionalGitLogRangeArg(flag: '--since' | '--until', value: string | undefined): string[] {
516
599
  return value ? [`${flag}=${value}`] : [];
517
600
  }
@@ -151,4 +151,5 @@ export type GitCommandName =
151
151
  | 'git_stash_push'
152
152
  | 'git_stash_pop'
153
153
  | 'git_checkout_files'
154
- | 'git_remote_url';
154
+ | 'git_remote_url'
155
+ | 'git_push';