@raidiant/notifai 7.0.0 → 7.0.2

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.
@@ -0,0 +1,99 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ const runOrcaCommand = (executable, args) => {
4
+ try {
5
+ return execFileSync(executable, args, {
6
+ encoding: 'utf8',
7
+ stdio: ['ignore', 'pipe', 'ignore'],
8
+ timeout: 2_000,
9
+ maxBuffer: 512 * 1024,
10
+ windowsHide: true,
11
+ }).trim();
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ };
17
+ function containsControlCharacter(value) {
18
+ return Array.from(value).some((character) => {
19
+ const codePoint = character.codePointAt(0) ?? 0;
20
+ return codePoint < 32 || codePoint === 127;
21
+ });
22
+ }
23
+ function worktreePathFromId(value) {
24
+ if (value.length === 0 || value.length > 4_096 || containsControlCharacter(value)) {
25
+ return null;
26
+ }
27
+ const separator = value.indexOf('::');
28
+ if (separator <= 0)
29
+ return null;
30
+ const repoId = value.slice(0, separator);
31
+ const worktreePath = value.slice(separator + 2);
32
+ if (!/^[A-Za-z0-9._-]+$/u.test(repoId) || worktreePath.length === 0)
33
+ return null;
34
+ return path.posix.isAbsolute(worktreePath) || path.win32.isAbsolute(worktreePath)
35
+ ? worktreePath
36
+ : null;
37
+ }
38
+ function orcaExecutable(env) {
39
+ const configured = env['ORCA_CLI_COMMAND']?.trim();
40
+ if (configured !== undefined && configured.length > 0) {
41
+ return containsControlCharacter(configured) ? null : configured;
42
+ }
43
+ return env['ORCA_DEV_REPO_ROOT'] === undefined ? 'orca' : 'orca-dev';
44
+ }
45
+ /**
46
+ * Read the semantic display name assigned to this exact Orca worktree.
47
+ *
48
+ * The environment contributes only an opaque selector. Orca must return that
49
+ * exact id and its matching path before its display name becomes trusted; no
50
+ * path component or identifier is ever promoted into the User-visible label.
51
+ */
52
+ export function readOrcaSessionTitle(env, command = runOrcaCommand) {
53
+ if (env['TERM_PROGRAM'] !== 'Orca')
54
+ return undefined;
55
+ const worktreeId = env['ORCA_WORKTREE_ID'];
56
+ if (worktreeId === undefined)
57
+ return undefined;
58
+ const expectedPath = worktreePathFromId(worktreeId);
59
+ if (expectedPath === null)
60
+ return undefined;
61
+ const executable = orcaExecutable(env);
62
+ if (executable === null)
63
+ return undefined;
64
+ let output;
65
+ try {
66
+ output = command(executable, [
67
+ 'worktree',
68
+ 'show',
69
+ '--worktree',
70
+ `id:${worktreeId}`,
71
+ '--json',
72
+ ]);
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ if (output === null || output === '')
78
+ return undefined;
79
+ let parsed;
80
+ try {
81
+ parsed = JSON.parse(output);
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ if (typeof parsed !== 'object' || parsed === null)
87
+ return undefined;
88
+ const response = parsed;
89
+ const worktree = response.result?.worktree;
90
+ if (response.ok !== true ||
91
+ worktree?.id !== worktreeId ||
92
+ worktree.path !== expectedPath ||
93
+ typeof worktree.displayName !== 'string' ||
94
+ worktree.displayName.trim().length === 0) {
95
+ return undefined;
96
+ }
97
+ return worktree.displayName;
98
+ }
99
+ //# sourceMappingURL=orca-session-title.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orca-session-title.js","sourceRoot":"","sources":["../src/orca-session-title.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raidiant/notifai",
3
- "version": "7.0.0",
3
+ "version": "7.0.2",
4
4
  "type": "module",
5
5
  "description": "Send native phone notifications from agents and local programs",
6
6
  "bin": {
@@ -13,6 +13,7 @@ import type { CredentialStore, MachineCredential } from './credentials.js'
13
13
  import { type HookAdapterTarget } from './hook-adapter.js'
14
14
  import { logConfigResolved, logSettingsFrom, nullLogger, type Logger } from './logging.js'
15
15
  import type { NativeSkills } from './native-skills.js'
16
+ import type { OrcaSessionTitleLookup } from './orca-session-title.js'
16
17
  import { packageVersion } from './release.js'
17
18
  import type { Tone } from './ui/theme.js'
18
19
 
@@ -81,6 +82,8 @@ export interface CommandDeps {
81
82
  codexSourcePid?: number
82
83
  /** Test seam and production adapter for the external native skills installer. */
83
84
  nativeSkills?: NativeSkills
85
+ /** Test seam for Orca's exact-worktree semantic title lookup. */
86
+ orcaSessionTitle?: OrcaSessionTitleLookup
84
87
  /**
85
88
  * The local record of what this invocation did. Optional so a test fake need
86
89
  * not carry one; `log(deps)` supplies a logger that records nothing.
@@ -151,12 +154,29 @@ export function authedClient(deps: CommandDeps, config: CliConfig): { client: Ap
151
154
 
152
155
  export const UPDATE_CLI_COMMAND = 'npm install -g @raidiant/notifai'
153
156
 
154
- function localRecovery(action: RecoveryAction | null): string | null {
157
+ function affectedDeviceNames(details: unknown): string[] {
158
+ if (typeof details !== 'object' || details === null || !('device_names' in details)) return []
159
+ const names = (details as { device_names?: unknown }).device_names
160
+ if (!Array.isArray(names)) return []
161
+ return [...new Set(names.filter((name): name is string => typeof name === 'string' && name.trim() !== ''))]
162
+ }
163
+
164
+ function naturalList(values: readonly string[]): string {
165
+ if (values.length <= 1) return values[0] ?? ''
166
+ if (values.length === 2) return `${values[0]} and ${values[1]}`
167
+ return `${values.slice(0, -1).join(', ')}, and ${values.at(-1)}`
168
+ }
169
+
170
+ function localRecovery(action: RecoveryAction | null, details: unknown): string | null {
155
171
  switch (action) {
156
172
  case 'update_cli':
157
173
  return `next: ${UPDATE_CLI_COMMAND}`
158
- case 'update_companion':
159
- return 'next: Update Notifai on the named device.'
174
+ case 'update_companion': {
175
+ const names = affectedDeviceNames(details)
176
+ return names.length > 0
177
+ ? `next: Update Notifai on ${naturalList(names)}.`
178
+ : 'next: Update Notifai on the affected devices.'
179
+ }
160
180
  case 'wait_for_service':
161
181
  return 'next: The service is being updated; try again later.'
162
182
  default:
@@ -213,7 +233,7 @@ export function reportError(
213
233
  'disagree about the contract; check with `notifai doctor`',
214
234
  )
215
235
  }
216
- const recovery = localRecovery(err.recoveryAction)
236
+ const recovery = localRecovery(err.recoveryAction, err.details)
217
237
  if (recovery !== null) deps.io.err(recovery)
218
238
  else if (err.recoveryAction === null && err.nextAction) deps.io.err(`next: ${err.nextAction}`)
219
239
  if (err.code === 'auth_required' || err.code === 'machine_revoked') return EXIT.auth
@@ -589,13 +589,14 @@ export async function doctorCommand(
589
589
  deps.io.out(`${mark} ${line(s)}`)
590
590
  }
591
591
  }
592
- await deps.io.outro?.(
593
- ok
594
- ? 'Everything looks good'
595
- : blocker?.id === 'contract'
596
- ? 'Update Notifai, then run doctor again'
597
- : `Start with: ${remedyLine(blocker)}`,
592
+ const surfacedUpdate = readiness.states.some(
593
+ (state) => state.id === 'contract' && (state.status === 'gap' || state.status === 'optional-gap'),
598
594
  )
595
+ if (blocker !== null && blocker.id !== 'contract') {
596
+ await deps.io.outro?.(`Start with: ${remedyLine(blocker)}`)
597
+ } else if (!surfacedUpdate) {
598
+ await deps.io.outro?.('Everything looks good')
599
+ }
599
600
  return ok ? EXIT.ok : EXIT.failed
600
601
  }
601
602
 
@@ -672,7 +672,6 @@ async function printInitClose(
672
672
  if (blocker?.id === 'contract') {
673
673
  deps.io.out(blocker.detail)
674
674
  deps.io.out(UPDATE_CLI_COMMAND)
675
- await deps.io.outro?.('Update Notifai, then run init again')
676
675
  return
677
676
  }
678
677
 
@@ -4,16 +4,40 @@ import { existsSync, readFileSync } from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { isRetryableReplyPollError, type ApiClient } from './client.js'
6
6
  import { buildSourceContext, inferInvocationContext } from './invocation-context.js'
7
+ import { readOrcaSessionTitle } from './orca-session-title.js'
7
8
  import { type DraftInvocation, type SendFlags } from './send.js'
8
9
  import { EXIT, reportError, type CommandDeps } from './commands-core.js'
9
10
  import type { ActiveHarnessSession } from './commands-harness-context.js'
10
11
 
12
+ function managedSessionTitle(
13
+ deps: CommandDeps,
14
+ flags: Pick<SendFlags, 'sessionLabel'>,
15
+ active: ActiveHarnessSession | null,
16
+ ): string | undefined {
17
+ if (active?.sessionLabel !== undefined) return active.sessionLabel
18
+ if (
19
+ active?.harness !== 'claude-code' ||
20
+ active.sessionId === undefined ||
21
+ active.sessionId !== deps.env['CLAUDE_CODE_SESSION_ID'] ||
22
+ flags.sessionLabel !== undefined ||
23
+ deps.env['NOTIFAI_SESSION_LABEL'] !== undefined
24
+ ) {
25
+ return undefined
26
+ }
27
+ try {
28
+ return (deps.orcaSessionTitle ?? readOrcaSessionTitle)(deps.env)
29
+ } catch {
30
+ return undefined
31
+ }
32
+ }
33
+
11
34
  export function resolveDraftInvocation(
12
35
  deps: CommandDeps,
13
36
  flags: Pick<SendFlags, 'sessionId' | 'sessionLabel'>,
14
37
  active: ActiveHarnessSession | null,
15
38
  ): { ok: true; invocation: DraftInvocation } | { ok: false; error: string } {
16
39
  const inferred = inferInvocationContext(deps.cwd)
40
+ const sessionTitle = managedSessionTitle(deps, flags, active)
17
41
  const source = buildSourceContext({
18
42
  env: deps.env,
19
43
  invocation: inferred,
@@ -25,7 +49,7 @@ export function resolveDraftInvocation(
25
49
  activeHarness: {
26
50
  harness: active.harness,
27
51
  ...(active.sessionId === undefined ? {} : { sessionId: active.sessionId }),
28
- ...(active.sessionLabel === undefined ? {} : { sessionLabel: active.sessionLabel }),
52
+ ...(sessionTitle === undefined ? {} : { sessionLabel: sessionTitle }),
29
53
  ...(active.sessionLabelPending === true ? { sessionLabelPending: true } : {}),
30
54
  },
31
55
  }),
@@ -180,6 +180,14 @@ class PlainInteractiveIo extends CapturedIo {
180
180
  interactive = true
181
181
  }
182
182
 
183
+ class OutroCapturedIo extends CapturedIo {
184
+ outros: string[] = []
185
+
186
+ async outro(message: string) {
187
+ this.outros.push(message)
188
+ }
189
+ }
190
+
183
191
  class InteractiveIo extends CapturedIo {
184
192
  interactive = true
185
193
  selectAnswer: string | null = 'global'
@@ -505,6 +513,86 @@ describe('command contracts', () => {
505
513
  expect(io.errLines.join('\n')).not.toContain('opaque-claude-session-42')
506
514
  })
507
515
 
516
+ it('uses the exact Orca worktree title for an active Claude session send', async () => {
517
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-orca-claude-title-'))
518
+ const io = new CapturedIo()
519
+ let submitted: SubmitNotificationRequestT | undefined
520
+ const client = {
521
+ submit: async (body: SubmitNotificationRequestT) => {
522
+ submitted = body
523
+ return receipt
524
+ },
525
+ } as unknown as ApiClient
526
+ const worktreeId = `repo-123::${cwd}`
527
+ const env = {
528
+ XDG_CONFIG_HOME: path.join(cwd, 'config'),
529
+ XDG_STATE_HOME: path.join(cwd, 'state'),
530
+ CLAUDECODE: '1',
531
+ CLAUDE_CODE_SESSION_ID: 'orca-claude-session',
532
+ TERM_PROGRAM: 'Orca',
533
+ ORCA_WORKTREE_ID: worktreeId,
534
+ }
535
+ const deps = {
536
+ ...makeDeps(io, client),
537
+ cwd,
538
+ env,
539
+ orcaSessionTitle: (lookupEnv: NodeJS.ProcessEnv) => {
540
+ expect(lookupEnv['ORCA_WORKTREE_ID']).toBe(worktreeId)
541
+ return 'Worker - semantic session implementation'
542
+ },
543
+ }
544
+
545
+ expect(
546
+ await sendCommand(deps, {
547
+ title: 'Resolver implemented',
548
+ body: 'The Orca worktree title was frozen locally.',
549
+ kind: 'done',
550
+ }),
551
+ ).toBe(EXIT.ok)
552
+
553
+ expect(submitted?.draft.source).toMatchObject({
554
+ session_id: 'orca-claude-session',
555
+ session_label: 'Worker - semantic session implementation',
556
+ harness: 'claude-code',
557
+ })
558
+ })
559
+
560
+ it('falls back safely when Orca returns a private path as its title', async () => {
561
+ const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-orca-private-title-'))
562
+ const io = new CapturedIo()
563
+ let submitted: SubmitNotificationRequestT | undefined
564
+ const client = {
565
+ submit: async (body: SubmitNotificationRequestT) => {
566
+ submitted = body
567
+ return receipt
568
+ },
569
+ } as unknown as ApiClient
570
+ const deps = {
571
+ ...makeDeps(io, client),
572
+ cwd,
573
+ env: {
574
+ XDG_CONFIG_HOME: path.join(cwd, 'config'),
575
+ XDG_STATE_HOME: path.join(cwd, 'state'),
576
+ CLAUDECODE: '1',
577
+ CLAUDE_CODE_SESSION_ID: 'orca-private-title-session',
578
+ TERM_PROGRAM: 'Orca',
579
+ ORCA_WORKTREE_ID: `repo-123::${cwd}`,
580
+ },
581
+ orcaSessionTitle: () => '/private/untrusted/worktree',
582
+ }
583
+
584
+ expect(
585
+ await sendCommand(deps, {
586
+ title: 'Resolver implemented',
587
+ body: 'Unsafe metadata must never become a visible name.',
588
+ kind: 'done',
589
+ }),
590
+ ).toBe(EXIT.ok)
591
+
592
+ expect(submitted?.draft.source?.session_label).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/)
593
+ expect(submitted?.draft.source?.session_label).not.toContain('/private/')
594
+ })
595
+
508
596
  it('uses the trusted OpenCode session title published by its managed adapter', async () => {
509
597
  const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-opencode-title-'))
510
598
  const io = new CapturedIo()
@@ -2938,7 +3026,7 @@ describe('compatibility-first update guidance', () => {
2938
3026
  listDevices: async () => ({ devices: [] }),
2939
3027
  accessStatus: async () => ({ email: 'user@example.test' }),
2940
3028
  } as unknown as ApiClient
2941
- const humanIo = new PlainInteractiveIo()
3029
+ const humanIo = new InteractiveIo()
2942
3030
  const deps = {
2943
3031
  ...makeDeps(humanIo, client),
2944
3032
  cwd,
@@ -2953,6 +3041,9 @@ describe('compatibility-first update guidance', () => {
2953
3041
  ).toBe(exit)
2954
3042
  expect(humanIo.outLines).toEqual(humanLines)
2955
3043
  expect(humanIo.errLines).toEqual([])
3044
+ expect(humanIo.outros).toEqual(
3045
+ status === 'ready' ? ['Everything looks good'] : [],
3046
+ )
2956
3047
 
2957
3048
  const jsonIo = new CapturedIo()
2958
3049
  expect(
@@ -3211,7 +3302,7 @@ describe('init', () => {
3211
3302
 
3212
3303
  it('resumes the same init after a required CLI update becomes current', async () => {
3213
3304
  const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-init-update-resume-'))
3214
- const io = new CapturedIo()
3305
+ const io = new OutroCapturedIo()
3215
3306
  let cliSupport = compatibilityWithCli({
3216
3307
  state: 'must_update',
3217
3308
  reason: 'minimum_not_met',
@@ -3246,11 +3337,13 @@ describe('init', () => {
3246
3337
  expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
3247
3338
  expect(io.outLines).toContain("Notifai can't send notifications until you update.")
3248
3339
  expect(io.outLines).toContain('npm install -g @raidiant/notifai')
3340
+ expect(io.outros).toEqual([])
3249
3341
  expect(submissions).toBe(0)
3250
3342
 
3251
3343
  cliSupport = currentCompatibility
3252
3344
  io.outLines = []
3253
3345
  io.errLines = []
3346
+ io.outros = []
3254
3347
 
3255
3348
  expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.ok)
3256
3349
  expect(submissions).toBe(1)
@@ -4555,6 +4648,39 @@ describe('asking before the hooks have ever run', () => {
4555
4648
  expect(readSessionState('codex-orchestrator', env).pending).toBeUndefined()
4556
4649
  })
4557
4650
 
4651
+ it('registers an Orca-managed Claude question under the semantic worktree title', () => {
4652
+ const cwd = scratchDir('notifai-orca-claude-ask-')
4653
+ const io = new CapturedIo()
4654
+ const env = {
4655
+ ...isolatedEnv(cwd),
4656
+ CLAUDECODE: '1',
4657
+ CLAUDE_CODE_SESSION_ID: 'orca-claude-question',
4658
+ TERM_PROGRAM: 'Orca',
4659
+ ORCA_WORKTREE_ID: `repo-123::${cwd}`,
4660
+ }
4661
+ const deps = {
4662
+ ...makeDeps(io, {} as ApiClient),
4663
+ cwd,
4664
+ env,
4665
+ now: () => 42,
4666
+ orcaSessionTitle: () => 'Worker - semantic session implementation',
4667
+ }
4668
+
4669
+ expect(hooksInstallCommand(deps, { harness: 'claude-code', execPath, scriptPath })).toBe(
4670
+ EXIT.ok,
4671
+ )
4672
+ writeSessionState('orca-claude-question', env, { last_prompt_at: 42, last_stop_at: 41 })
4673
+ writeProjectSession(cwd, env, 'orca-claude-question', 42, 'claude-code')
4674
+ io.outLines = []
4675
+
4676
+ expect(askCommand(deps, 'Ship the semantic resolver?', {})).toBe(EXIT.ok)
4677
+ expect(readSessionState('orca-claude-question', env).pending?.[0]?.source).toMatchObject({
4678
+ session_id: 'orca-claude-question',
4679
+ session_label: 'Worker - semantic session implementation',
4680
+ harness: 'claude-code',
4681
+ })
4682
+ })
4683
+
4558
4684
  it('judges the fired check against the active harness, not a global installation of another', async () => {
4559
4685
  const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-fired-active-harness-'))
4560
4686
  const io = new CapturedIo()
@@ -5457,7 +5583,7 @@ describe('command failures carrying server details', () => {
5457
5583
 
5458
5584
  expect(io.errLines).toEqual([
5459
5585
  "feature_unavailable: The selected device can't answer questions.",
5460
- 'next: Update Notifai on the named device.',
5586
+ 'next: Update Notifai on Old phone.',
5461
5587
  ])
5462
5588
  expect(io.errLines.join(' ')).not.toContain('untrusted')
5463
5589
  expect(io.errLines.join(' ')).not.toContain('disagree about the contract')
@@ -199,7 +199,7 @@ const INFO: Record<ConfigKey, Omit<ConfigKeyInfo, 'key'>> = {
199
199
  kind: 'string',
200
200
  summary: 'Free text an agent reads before deciding whether to notify you',
201
201
  detail:
202
- 'Your own guidance, in your own words, about what is worth a notification in this project — read by agents before they send. Something like "only tell me about failures and finished deploys; never per-file progress".\n\nIt is advice to the agent, not a filter the service enforces.',
202
+ 'Your own guidance, in your own words, about what is worth a notification in this project — read by agents before they send. Something like "only tell me about failures and finished deploys; never per-file progress".\n\nIt is advice to the agent, not a filter the service enforces. Agents follow it as your literal standing word, so an agent may write it only when you ask for a preference to be remembered — and only in your words, never its own summary.',
203
203
  unsetMeans: 'agents fall back to their default judgement',
204
204
  example: 'Only failures and completed deploys',
205
205
  },
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { readOrcaSessionTitle, type OrcaCommand } from './orca-session-title.js'
3
+
4
+ const worktreeId = 'repo-123::/worktrees/semantic-session-implementation'
5
+
6
+ function response(overrides: Record<string, unknown> = {}): string {
7
+ return JSON.stringify({
8
+ ok: true,
9
+ result: {
10
+ worktree: {
11
+ id: worktreeId,
12
+ path: '/worktrees/semantic-session-implementation',
13
+ displayName: 'Worker - semantic session implementation',
14
+ ...overrides,
15
+ },
16
+ },
17
+ })
18
+ }
19
+
20
+ describe('Orca semantic session title', () => {
21
+ it('accepts only the display name returned for the exact managed worktree', () => {
22
+ const calls: { executable: string; args: readonly string[] }[] = []
23
+ const command: OrcaCommand = (executable, args) => {
24
+ calls.push({ executable, args })
25
+ return response()
26
+ }
27
+
28
+ expect(
29
+ readOrcaSessionTitle(
30
+ { TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: worktreeId },
31
+ command,
32
+ ),
33
+ ).toBe('Worker - semantic session implementation')
34
+ expect(calls).toEqual([
35
+ {
36
+ executable: 'orca',
37
+ args: ['worktree', 'show', '--worktree', `id:${worktreeId}`, '--json'],
38
+ },
39
+ ])
40
+ })
41
+
42
+ it('uses Orca-provided executable selection without invoking a shell', () => {
43
+ const calls: string[] = []
44
+ expect(
45
+ readOrcaSessionTitle(
46
+ {
47
+ TERM_PROGRAM: 'Orca',
48
+ ORCA_WORKTREE_ID: worktreeId,
49
+ ORCA_CLI_COMMAND: '/mnt/c/Program Files/Orca/orca.exe',
50
+ },
51
+ (executable) => {
52
+ calls.push(executable)
53
+ return response()
54
+ },
55
+ ),
56
+ ).toBe('Worker - semantic session implementation')
57
+ expect(calls).toEqual(['/mnt/c/Program Files/Orca/orca.exe'])
58
+ })
59
+
60
+ it('ignores non-Orca and malformed selectors without running a command', () => {
61
+ const command: OrcaCommand = () => {
62
+ throw new Error('must not run')
63
+ }
64
+ expect(readOrcaSessionTitle({ ORCA_WORKTREE_ID: worktreeId }, command)).toBeUndefined()
65
+ expect(
66
+ readOrcaSessionTitle(
67
+ { TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: '/private/untrusted' },
68
+ command,
69
+ ),
70
+ ).toBeUndefined()
71
+ expect(
72
+ readOrcaSessionTitle(
73
+ { TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: 'repo-123::relative/path' },
74
+ command,
75
+ ),
76
+ ).toBeUndefined()
77
+ })
78
+
79
+ it.each([
80
+ ['command failure', () => null],
81
+ ['thrown command failure', () => { throw new Error('offline') }],
82
+ ['malformed JSON', () => '{not json}'],
83
+ ['failed response', () => JSON.stringify({ ok: false })],
84
+ ['different worktree id', () => response({ id: 'repo-123::/worktrees/other' })],
85
+ ['different worktree path', () => response({ path: '/private/untrusted' })],
86
+ ['blank display name', () => response({ displayName: ' ' })],
87
+ ])('fails closed for %s', (_name, command) => {
88
+ expect(
89
+ readOrcaSessionTitle(
90
+ { TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: worktreeId },
91
+ command,
92
+ ),
93
+ ).toBeUndefined()
94
+ })
95
+ })
@@ -0,0 +1,105 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import path from 'node:path'
3
+
4
+ export type OrcaSessionTitleLookup = (env: NodeJS.ProcessEnv) => string | undefined
5
+ export type OrcaCommand = (executable: string, args: readonly string[]) => string | null
6
+
7
+ const runOrcaCommand: OrcaCommand = (executable, args) => {
8
+ try {
9
+ return execFileSync(executable, args, {
10
+ encoding: 'utf8',
11
+ stdio: ['ignore', 'pipe', 'ignore'],
12
+ timeout: 2_000,
13
+ maxBuffer: 512 * 1024,
14
+ windowsHide: true,
15
+ }).trim()
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ function containsControlCharacter(value: string): boolean {
22
+ return Array.from(value).some((character) => {
23
+ const codePoint = character.codePointAt(0) ?? 0
24
+ return codePoint < 32 || codePoint === 127
25
+ })
26
+ }
27
+
28
+ function worktreePathFromId(value: string): string | null {
29
+ if (value.length === 0 || value.length > 4_096 || containsControlCharacter(value)) {
30
+ return null
31
+ }
32
+ const separator = value.indexOf('::')
33
+ if (separator <= 0) return null
34
+ const repoId = value.slice(0, separator)
35
+ const worktreePath = value.slice(separator + 2)
36
+ if (!/^[A-Za-z0-9._-]+$/u.test(repoId) || worktreePath.length === 0) return null
37
+ return path.posix.isAbsolute(worktreePath) || path.win32.isAbsolute(worktreePath)
38
+ ? worktreePath
39
+ : null
40
+ }
41
+
42
+ function orcaExecutable(env: NodeJS.ProcessEnv): string | null {
43
+ const configured = env['ORCA_CLI_COMMAND']?.trim()
44
+ if (configured !== undefined && configured.length > 0) {
45
+ return containsControlCharacter(configured) ? null : configured
46
+ }
47
+ return env['ORCA_DEV_REPO_ROOT'] === undefined ? 'orca' : 'orca-dev'
48
+ }
49
+
50
+ /**
51
+ * Read the semantic display name assigned to this exact Orca worktree.
52
+ *
53
+ * The environment contributes only an opaque selector. Orca must return that
54
+ * exact id and its matching path before its display name becomes trusted; no
55
+ * path component or identifier is ever promoted into the User-visible label.
56
+ */
57
+ export function readOrcaSessionTitle(
58
+ env: NodeJS.ProcessEnv,
59
+ command: OrcaCommand = runOrcaCommand,
60
+ ): string | undefined {
61
+ if (env['TERM_PROGRAM'] !== 'Orca') return undefined
62
+ const worktreeId = env['ORCA_WORKTREE_ID']
63
+ if (worktreeId === undefined) return undefined
64
+ const expectedPath = worktreePathFromId(worktreeId)
65
+ if (expectedPath === null) return undefined
66
+ const executable = orcaExecutable(env)
67
+ if (executable === null) return undefined
68
+
69
+ let output: string | null
70
+ try {
71
+ output = command(executable, [
72
+ 'worktree',
73
+ 'show',
74
+ '--worktree',
75
+ `id:${worktreeId}`,
76
+ '--json',
77
+ ])
78
+ } catch {
79
+ return undefined
80
+ }
81
+ if (output === null || output === '') return undefined
82
+
83
+ let parsed: unknown
84
+ try {
85
+ parsed = JSON.parse(output)
86
+ } catch {
87
+ return undefined
88
+ }
89
+ if (typeof parsed !== 'object' || parsed === null) return undefined
90
+ const response = parsed as {
91
+ ok?: unknown
92
+ result?: { worktree?: { id?: unknown; path?: unknown; displayName?: unknown } }
93
+ }
94
+ const worktree = response.result?.worktree
95
+ if (
96
+ response.ok !== true ||
97
+ worktree?.id !== worktreeId ||
98
+ worktree.path !== expectedPath ||
99
+ typeof worktree.displayName !== 'string' ||
100
+ worktree.displayName.trim().length === 0
101
+ ) {
102
+ return undefined
103
+ }
104
+ return worktree.displayName
105
+ }