@vodmal/vdx-cli 0.35.0 → 0.37.0

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.
@@ -31,6 +31,8 @@ import { at, dict, str } from './narrow.ts';
31
31
  /** The opening of `vdx ai --check`'s text; the label is an ssh destination from the other machines. */
32
32
  const MACHINE_LINE_PREFIX = 'vdx ai: this machine is';
33
33
  const MACHINE_LINE_RE = /^vdx ai: this machine is `([^`]+)`/m;
34
+ /** The opening of the line `vdx ai --check` adds in a mail session (`vdx ai --intercom`). */
35
+ export const MAIL_LINE_PREFIX = "· this is the project's mail session";
34
36
  /** A label read from a synced file goes to ssh: it must not be able to read as an option. */
35
37
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
36
38
  const ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -58,6 +60,8 @@ export interface Conversation {
58
60
  typedHere: boolean;
59
61
  /** Claude's title for it, else the last prompt. */
60
62
  title: string | null;
63
+ /** Last started as the project's mail session (`vdx ai --intercom`): not the person's to continue. */
64
+ mail: boolean;
61
65
  }
62
66
 
63
67
  export function claudeConfigDir(env: NodeJS.ProcessEnv, home: string): string {
@@ -78,6 +82,7 @@ export function readConversation(file: string, updated: Date): Conversation | nu
78
82
  return null;
79
83
  }
80
84
  let machine: string | null = null;
85
+ let mail = false;
81
86
  let aiTitle: string | null = null;
82
87
  let lastPrompt: string | null = null;
83
88
  let turns = false;
@@ -102,13 +107,17 @@ export function readConversation(file: string, updated: Date): Conversation | nu
102
107
  const content = at(entry, 'attachment', 'content');
103
108
  for (const part of Array.isArray(content) ? (content as unknown[]) : [content]) {
104
109
  const label = typeof part === 'string' ? part.match(MACHINE_LINE_RE)?.[1] : undefined;
105
- if (label && LABEL_RE.test(label)) machine = label;
110
+ if (label && LABEL_RE.test(label)) {
111
+ machine = label;
112
+ // The same text says whether that start was a mail session's.
113
+ mail = (part as string).includes(MAIL_LINE_PREFIX);
114
+ }
106
115
  }
107
116
  }
108
117
  }
109
118
  if (!turns) return null;
110
119
  const id = path.basename(file, '.jsonl');
111
- return { id, updated, machine, typedHere: false, title: aiTitle ?? lastPrompt };
120
+ return { id, updated, machine, typedHere: false, title: aiTitle ?? lastPrompt, mail };
112
121
  }
113
122
 
114
123
  /** Which of `ids` had a prompt typed in them on this machine. */
package/src/doctor.ts CHANGED
@@ -4,9 +4,24 @@ import * as path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { execFileSync } from 'node:child_process';
6
6
  import { effectiveAuthor, isGitRepo, rankAuthors, remoteParts, scanPool } from './author.ts';
7
- import { authorPoolDirs, loadEnvironment, resolveEnvironmentPath, shellQuote, type Environment } from './ai.ts';
7
+ import {
8
+ authorPoolDirs,
9
+ loadEnvironment,
10
+ resolveEnvironmentPath,
11
+ resolveProjectRoot,
12
+ shellQuote,
13
+ type Environment,
14
+ } from './ai.ts';
8
15
  import { checkRepoHooks } from './clone-checks.ts';
9
16
  import { githubToken, machineIo, type HostingIo } from './hosting.ts';
17
+ import {
18
+ describeMemory,
19
+ fixProjectMemory,
20
+ memoryNeedsFix,
21
+ memoryState,
22
+ type MemoryCtx,
23
+ type MemoryState,
24
+ } from './memory.ts';
10
25
  import { at, dict, errorMessage, str } from './narrow.ts';
11
26
  import {
12
27
  checkPersonalHooks,
@@ -24,6 +39,8 @@ export interface CheckResult {
24
39
  level?: number;
25
40
  message: string;
26
41
  remedy?: string;
42
+ /** For the agent's own session (`--check`): what the fix does not change for it. */
43
+ sessionNote?: string;
27
44
  }
28
45
 
29
46
  /**
@@ -473,6 +490,52 @@ export function checkGithubToken(
473
490
  };
474
491
  }
475
492
 
493
+ /** The profile, or none: a broken one is `vdx ai`'s report, not a reason for doctor to stop. */
494
+ function profile(): Environment {
495
+ const file = resolveEnvironmentPath();
496
+ try {
497
+ return file ? loadEnvironment(file) : {};
498
+ } catch {
499
+ return {};
500
+ }
501
+ }
502
+
503
+ /** Claude Code is the project's agent: the profile names it, or names no agent at all. */
504
+ function claudeIsTheAgent(environment: Environment): boolean {
505
+ return path.basename(environment.agent?.command ?? 'claude') === 'claude';
506
+ }
507
+
508
+ function memoryCtx(): MemoryCtx {
509
+ return { env: process.env, home: os.homedir() };
510
+ }
511
+
512
+ /**
513
+ * Where Claude Code keeps the project's memory (claude-memory-in-project): inside the
514
+ * project, `.claude/memory/`, so a moved folder takes it along — and the path set there
515
+ * is the trace a move leaves.
516
+ */
517
+ export function checkClaudeMemory(
518
+ ctx: DoctorCtx,
519
+ environment: Environment = profile(),
520
+ mctx: MemoryCtx = memoryCtx(),
521
+ ): CheckResult | null {
522
+ if (ctx.projectRoot === null || !claudeIsTheAgent(environment)) return null;
523
+ // Claude Code's slot is the folder the agent starts in — the checkout's top for vdx ai.
524
+ const root = resolveProjectRoot(ctx.projectRoot) ?? ctx.projectRoot;
525
+ const state = memoryState(root, mctx);
526
+ return {
527
+ id: 'claude-memory',
528
+ label: 'Claude memory',
529
+ status: memoryNeedsFix(state) ? 'warning' : 'ok',
530
+ message: describeMemory(state, mctx),
531
+ ...(memoryNeedsFix(state) ? { remedy: `vdx doctor --fix ${shellQuote(root)}` } : {}),
532
+ // A session read the place when it started: after a fix it keeps writing to the old one.
533
+ ...(state.kind === 'outside' || state.kind === 'moved' || state.kind === 'elsewhere'
534
+ ? { sessionNote: 'This session keeps the old place until it restarts (vdx ai --restart).' }
535
+ : {}),
536
+ };
537
+ }
538
+
476
539
  const CHECKS: Array<(ctx: DoctorCtx) => CheckResult | null> = [
477
540
  checkVdxVersion,
478
541
  checkVdx,
@@ -487,6 +550,7 @@ const CHECKS: Array<(ctx: DoctorCtx) => CheckResult | null> = [
487
550
  (ctx) => checkPersonalHooks(ctx.projectRoot),
488
551
  checkGitAuthor,
489
552
  (ctx) => checkGithubToken(ctx),
553
+ (ctx) => checkClaudeMemory(ctx),
490
554
  ];
491
555
 
492
556
  export function resolveDoctorCtx(cwd: string = process.cwd()): DoctorCtx {
@@ -510,13 +574,20 @@ export function runDoctor(ctx: DoctorCtx = resolveDoctorCtx()): DoctorReport {
510
574
  export function runDoctorCheck(
511
575
  ctx: DoctorCtx = resolveDoctorCtx(),
512
576
  deps: PersonalHookDeps = defaultPersonalHookDeps(),
577
+ environment: Environment = profile(),
578
+ mctx: MemoryCtx = memoryCtx(),
513
579
  ): string {
514
- const rows = [checkRepoHooks(ctx.projectRoot), checkPersonalHooks(ctx.projectRoot, deps)].filter(
515
- (c): c is CheckResult => c !== null && c.status !== 'ok',
516
- );
580
+ const rows = [
581
+ checkRepoHooks(ctx.projectRoot),
582
+ checkPersonalHooks(ctx.projectRoot, deps),
583
+ checkClaudeMemory(ctx, environment, mctx),
584
+ ].filter((c): c is CheckResult => c !== null && c.status !== 'ok');
517
585
  return rows
518
586
  .map(
519
- (c) => `vdx doctor: ${c.label} — ${c.message}.` + (c.remedy ? ` Fix, on the user's word: \`${c.remedy}\`.` : ''),
587
+ (c) =>
588
+ `vdx doctor: ${c.label} — ${c.message}.` +
589
+ (c.remedy ? ` Fix, on the user's word: \`${c.remedy}\`.` : '') +
590
+ (c.sessionNote ? ` ${c.sessionNote}` : ''),
520
591
  )
521
592
  .join('\n');
522
593
  }
@@ -530,6 +601,18 @@ export function runDoctorFix(deps: PersonalHookDeps = defaultPersonalHookDeps())
530
601
  return fixPersonalHooks(deps);
531
602
  }
532
603
 
604
+ /** `vdx doctor --fix` for the memory: the profile's personal hosts, this machine's Claude Code. */
605
+ export function fixDoctorMemory(
606
+ projectRoot: string,
607
+ state: MemoryState,
608
+ io: { ask: (prompt: string) => string | null; log: (line: string) => void; today: string },
609
+ ): string[] {
610
+ return fixProjectMemory(projectRoot, state, memoryCtx(), {
611
+ ...io,
612
+ personalHosts: profile().git?.personal_hosts ?? [],
613
+ });
614
+ }
615
+
533
616
  const PROJECT_MARKER_FILES = [
534
617
  'package.json',
535
618
  'composer.json',
package/src/hosting.ts CHANGED
@@ -332,7 +332,7 @@ function judgeGitlab(io: HostingIo, host: string, repo: string, token: string |
332
332
  interface BitbucketAuth {
333
333
  token: string;
334
334
  scheme: 'Bearer' | 'Basic';
335
- source: string;
335
+ source: 'BITBUCKET_TOKEN' | 'the login git keeps';
336
336
  }
337
337
 
338
338
  const MIN_BUILDS = 'com.atlassian.bitbucket.server.bitbucket-build:requiredBuildsMergeCheck';
@@ -340,14 +340,37 @@ const MIN_BUILDS = 'com.atlassian.bitbucket.server.bitbucket-build:requiredBuild
340
340
  /** A refusal of Bitbucket as the reason the answer is unknown; null for a 200. */
341
341
  function bitbucketRefusal(a: Answer, host: string, repo: string, auth: BitbucketAuth | null): Unknown | null {
342
342
  if (a.status === 200) return null;
343
- const message = str(at(list(at(a.body, 'errors'))[0], 'message')) ?? '';
343
+ const error = list(at(a.body, 'errors'))[0];
344
+ const message = str(at(error, 'message')) ?? '';
345
+ const login = `git ls-remote https://${host}/scm/${repo}.git, a token as the password`;
344
346
  if (a.status === 401 && !auth) {
345
347
  return {
346
- unknown: `${host} shows ${repo} only after a login — once: git ls-remote https://${host}/scm/${repo}.git, a token as the password; or BITBUCKET_TOKEN with BITBUCKET_HOST`,
348
+ unknown: `${host} shows ${repo} only after a login — once: ${login}; or BITBUCKET_TOKEN with BITBUCKET_HOST`,
347
349
  };
348
350
  }
349
- if (a.status === 401) {
350
- return { unknown: `${host} refused ${repo} to ${auth?.source} — no access, a changed password, a revoked token?` };
351
+ if (a.status === 401 && auth) {
352
+ // A login the server refused — a revoked token, a changed password — is an
353
+ // AuthenticationException; one it took, without access, an AuthorisationException,
354
+ // as is no login at all, and a Bearer token it does not know (git.finam.ru, 2026-10-22).
355
+ // vdx does not touch git's store: the owner erases the login (Sidetrack #11).
356
+ const exception = str(at(error, 'exceptionName')) ?? '';
357
+ if (
358
+ exception === 'com.atlassian.bitbucket.auth.IncorrectPasswordAuthenticationException' &&
359
+ auth.source !== 'BITBUCKET_TOKEN'
360
+ ) {
361
+ return {
362
+ unknown: `${host} refused the login git keeps for it, and every audit sends it again — each refusal brings the account nearer a CAPTCHA; erase it: printf 'protocol=https\\nhost=${host}\\n\\n' | git credential reject — and log in once more: ${login}`,
363
+ };
364
+ }
365
+ if (exception.endsWith('AuthenticationException')) {
366
+ return { unknown: `${host} refused ${auth.source}${message ? `: ${message}` : ''}` };
367
+ }
368
+ return {
369
+ unknown:
370
+ auth.source === 'BITBUCKET_TOKEN'
371
+ ? `${host} refused ${repo} to BITBUCKET_TOKEN — no access, or a token it does not know: it takes one for no login`
372
+ : `${host} refused ${repo} to the login git keeps — it has no access to the repository`,
373
+ };
351
374
  }
352
375
  return { unknown: `${host} answered ${a.status} for ${repo}${message ? `: ${message}` : ''}` };
353
376
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import * as os from 'node:os';
2
3
  import * as path from 'node:path';
3
4
  import { execFileSync } from 'node:child_process';
4
5
  import { loadRubric } from './rubric.ts';
@@ -16,6 +17,8 @@ import {
16
17
  reportDoctorAnsi,
17
18
  } from './report.ts';
18
19
  import {
20
+ checkClaudeMemory,
21
+ fixDoctorMemory,
19
22
  runDoctor,
20
23
  runDoctorCheck,
21
24
  runDoctorFix,
@@ -23,6 +26,7 @@ import {
23
26
  looksLikeProject,
24
27
  readCliVersion,
25
28
  } from './doctor.ts';
29
+ import { memoryState } from './memory.ts';
26
30
  import { planInit, writeInit, renderPlanSummary } from './init.ts';
27
31
  import {
28
32
  planPublish,
@@ -35,7 +39,17 @@ import {
35
39
  } from './publish.ts';
36
40
  import { resolveDefaultRubric } from './defaults.ts';
37
41
  import { defaultGateDeps, GATE_PREFIX, planGate, pushThroughGate } from './gate.ts';
38
- import { askLine, runAi, runAiCheck, runAiRemote, defaultDeps, hostLabel, REMOTE_HOST_RE } from './ai.ts';
42
+ import {
43
+ askLine,
44
+ runAi,
45
+ runAiCheck,
46
+ runAiClose,
47
+ runAiRemote,
48
+ defaultDeps,
49
+ hostLabel,
50
+ REMOTE_HOST_RE,
51
+ resolveProjectRoot,
52
+ } from './ai.ts';
39
53
  import { isConversationId } from './conversations.ts';
40
54
 
41
55
  const DEFAULT_RUBRIC = resolveDefaultRubric();
@@ -90,11 +104,27 @@ the project's files). Hand a person \`vdx ai\`, not \`<agent> --<flag>\`.
90
104
  rules marked \`wakes: true\` (a channel flag) are left out. Back
91
105
  to it: --focused --conversation <id>. A running agent keeps its
92
106
  mode: plain vdx ai attaches to it as it is
107
+ --intercom the project's mail session: a new conversation in a tmux
108
+ session of its own (<name>-mail), one per project and machine —
109
+ a running one is attached to, never a second. The agent, its
110
+ hooks and MCP servers get VDX_INTERCOM=1: letters and wakes for
111
+ the project go there, and your own session stays quiet; the
112
+ profile's \`wakes\` rules stay. It starts with agent.intercom_args
113
+ (its first prompt). Every other start leaves it alone, and its
114
+ conversations are not continued unless named with --conversation.
115
+ Started by the agent that has a letter and nobody to wake:
116
+ vdx ai --intercom --detach <project>
117
+ --close [--waiting <n>]
118
+ inside the mail session: it ends itself — its pane closes 2 s
119
+ later. --waiting <n>: n questions to you still unanswered; the
120
+ project's other tmux sessions here get @vdx_waiting = n (without
121
+ it the mark is cleared). Anywhere else: refused, nothing closed
93
122
  --conversation <id>
94
123
  continue this Claude Code conversation: attach to the agent in
95
124
  it, or start one next to the agents running here
96
125
  --restart restart the running agent in its pane — applies a changed profile;
97
- with --new, --focused or --conversation, it is replaced by that.
126
+ with --new, --focused or --conversation, it is replaced by that;
127
+ with --intercom, the mail session gets a new conversation.
98
128
  Without it, an agent running without the profile's flags is
99
129
  a question in a terminal: Enter attaches to it as is, r
100
130
  restarts it, q leaves it
@@ -116,6 +146,11 @@ ones marked — Enter takes the newest; without a terminal it takes this
116
146
  machine's. A new conversation (--new, or none to continue) while an agent of
117
147
  the project runs on another machine starts only after a yes.
118
148
 
149
+ Claude Code's memory of the project (Claude Code only): when it is not inside the
150
+ project, .claude/memory/, or the folder was moved, a start in a terminal asks to
151
+ put it there — Enter does, as vdx doctor --fix; with an agent of the project
152
+ running, or no terminal, it only names that command.
153
+
119
154
  Exit codes: 0 the agent runs per the profile; 2 profile or usage error;
120
155
  3 the running agent lacks profile flags, or is not focused under --focused,
121
156
  and was left as is — no terminal, or q (rerun with --restart); 4 the start
@@ -166,8 +201,9 @@ mise.toml is overwritten only with --force.
166
201
  Ctrl-C there stops only the wait).
167
202
  Nothing in the package's directory changed since the tag of its version: the
168
203
  same package under a new number — at a terminal a question, Enter releases nothing.
204
+ Without a terminal it releases nothing: npm would ask for an OTP nobody sees.
169
205
  --dry-run runs the pre-flight and prints the plan; --force goes on past a failed
170
- pre-flight; --no-push leaves the commit and the tag local.
206
+ pre-flight, and without a terminal; --no-push leaves the commit and the tag local.
171
207
  `,
172
208
  bools: ['dry-run', 'force', 'no-push'],
173
209
  positionals: 1,
@@ -204,25 +240,35 @@ Claude Code plugin) and, inside a project (default: cwd), its commit author and
204
240
  its git hooks: are the hooks the repository declares on in this clone, and do
205
241
  the personal hooks of your profile (git.hooks) run on this machine; with origin on
206
242
  GitHub, the token vdx asks GitHub with ($GH_TOKEN, $GITHUB_TOKEN or gh) and
207
- whether it works — without one GitHub answers 60 requests an hour. Reads only.
208
- Exit 2 when something is missing.
243
+ whether it works — without one GitHub answers 60 requests an hour; and where
244
+ Claude Code keeps the project's memory: inside it, .claude/memory/, a moved folder
245
+ takes it along. Reads only, unless --fix. Exit 2 when something is missing.
209
246
 
210
247
  --check for the agent's own session (the vdx plugin's SessionStart hook): one
211
- line per hook that is declared but does not run here; nothing when in order
248
+ line per hook that is declared but does not run here, and one when the
249
+ memory is not inside; nothing when in order
212
250
  --fix write the profile's personal hooks into ~/.gitconfig where they are
213
- missing or differ; repository hooks are switched on by the project's
214
- install step, which the report names
251
+ missing or differ (repository hooks are switched on by the project's
252
+ install step, which the report names); and put the memory inside:
253
+ notes from ~/.claude/projects/<path>/memory into .claude/memory/, none
254
+ overwritten, the old place renamed, autoMemoryDirectory written —
255
+ into .claude/settings.json in a personal project (no origin, or origin
256
+ on a host of git.personal_hosts), else into .claude/settings.local.json,
257
+ with the memory kept out of git (.git/info/exclude) unless you say
258
+ yes. After a folder is moved: the path follows it, and the project's
259
+ conversations move to the new path
215
260
  `,
216
261
  bools: ['json', 'check', 'fix'],
217
262
  values: ['format'],
218
263
  positionals: 1,
219
264
  },
220
265
  ai: {
221
- usage: 'vdx ai[@host] [project_path] [--new | --conversation <id>] [--focused] [--restart] [--detach] [--dry-run]',
266
+ usage:
267
+ 'vdx ai[@host] [project_path] [--new | --conversation <id>] [--focused | --intercom] [--restart] [--detach] [--dry-run]',
222
268
  help: AI_HELP,
223
269
  // `--resume` asked to continue in 0.12; continuing is the default now.
224
- bools: ['new', 'focused', 'restart', 'resume', 'detach', 'dry-run', 'check'],
225
- values: ['conversation'],
270
+ bools: ['new', 'focused', 'intercom', 'restart', 'resume', 'detach', 'dry-run', 'check', 'close'],
271
+ values: ['conversation', 'waiting'],
226
272
  positionals: 1,
227
273
  },
228
274
  ...Object.fromEntries(
@@ -412,6 +458,18 @@ function cmdPublish(opts: ParsedArgs): void {
412
458
  const bump = bumpArg as BumpKind;
413
459
  const projectRoot = process.cwd();
414
460
 
461
+ // npm asks for an OTP or a confirmation in the browser, after the bump: without a
462
+ // terminal nobody answers. Claude Code's Bash tool has none, and `!` there moves a
463
+ // command past 120 s to the background (publish-npm-outage-preflight, 2026-10-22).
464
+ if (!opts.flags['dry-run'] && !opts.flags.force && process.stdin.isTTY !== true) {
465
+ process.stderr.write(
466
+ `vdx publish: no terminal here — npm asks for a one-time password or a confirmation in the browser, ` +
467
+ `and nobody could answer. Run it in your own terminal: vdx publish ${bump}. ` +
468
+ `--dry-run shows the plan; --force releases without a terminal (an automation token)\n`,
469
+ );
470
+ process.exit(2);
471
+ }
472
+
415
473
  const manifest = loadManifest(projectRoot);
416
474
  const overrides = loadOverrides(projectRoot);
417
475
  const rubric = loadRubric(DEFAULT_RUBRIC);
@@ -568,6 +626,22 @@ function cmdDoctor(opts: ParsedArgs): void {
568
626
  process.stderr.write(`vdx doctor --fix: ${errorMessage(e)}\n`);
569
627
  process.exit(2);
570
628
  }
629
+ const root = ctx.projectRoot ? resolveProjectRoot(ctx.projectRoot) : null;
630
+ const memory = root ? checkClaudeMemory({ projectRoot: root }) : null;
631
+ if (root && memory?.remedy) {
632
+ try {
633
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
634
+ const done = fixDoctorMemory(root, memoryState(root, { env: process.env, home: os.homedir() }), {
635
+ ask: (prompt) => (interactive ? askLine(prompt) : null),
636
+ log: (line) => process.stderr.write(line + '\n'),
637
+ today: new Date().toLocaleDateString('sv-SE'),
638
+ });
639
+ process.stderr.write(done.map((d) => `✓ ${d}`).join('\n') + (done.length ? '\n' : ''));
640
+ } catch (e) {
641
+ process.stderr.write(`vdx doctor --fix: Claude memory — ${errorMessage(e)}\n`);
642
+ process.exit(2);
643
+ }
644
+ }
571
645
  }
572
646
  const report = runDoctor(ctx);
573
647
 
@@ -602,6 +676,27 @@ function cmdAi(opts: ParsedArgs): void {
602
676
  }
603
677
  process.exit(runAiCheck(opts.positionals[0] ?? '.', defaultDeps()));
604
678
  }
679
+ if (opts.flags.close === true || opts.flags.waiting !== undefined) {
680
+ // Run inside the mail session, about that session: nothing to start, no other machine.
681
+ const others = Object.keys(opts.flags).filter((f) => f !== 'close' && f !== 'waiting');
682
+ if (opts.flags.close !== true || others.length > 0 || opts.cmd !== 'ai') {
683
+ process.stderr.write(
684
+ opts.flags.close !== true
685
+ ? 'vdx ai: --waiting goes with --close\n'
686
+ : `vdx ai: --close takes only the project path and --waiting (not ${opts.cmd !== 'ai' ? opts.cmd : `--${others.join(', --')}`})\n`,
687
+ );
688
+ process.exit(2);
689
+ }
690
+ const raw = opts.flags.waiting;
691
+ const waiting = raw === undefined ? 0 : typeof raw === 'string' && /^\d+$/.test(raw) ? Number(raw) : NaN;
692
+ if (Number.isNaN(waiting)) {
693
+ process.stderr.write(
694
+ `vdx ai: --waiting takes the number of questions to the user (0, 1, 2…), not "${String(raw)}"\n`,
695
+ );
696
+ process.exit(2);
697
+ }
698
+ process.exit(runAiClose(opts.positionals[0] ?? '.', waiting, defaultDeps()));
699
+ }
605
700
  const conversation = typeof opts.flags.conversation === 'string' ? opts.flags.conversation : undefined;
606
701
  if (conversation !== undefined && !isConversationId(conversation)) {
607
702
  process.stderr.write(`vdx ai: --conversation takes a conversation id (a UUID), not "${conversation}"\n`);
@@ -619,6 +714,7 @@ function cmdAi(opts: ParsedArgs): void {
619
714
  detach: opts.flags.detach === true,
620
715
  dryRun: opts.flags['dry-run'] === true,
621
716
  focused: opts.flags.focused === true,
717
+ intercom: opts.flags.intercom === true,
622
718
  };
623
719
  const deps = { ...defaultDeps(), version: readCliVersion() };
624
720
  // `ai@m3`: the same command on another machine. Its own label runs here.
package/src/init.ts CHANGED
@@ -6,7 +6,7 @@ import { at } from './narrow.ts';
6
6
  export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
7
7
  export type Verb = (typeof STANDARD_VERBS)[number];
8
8
 
9
- export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.5.0';
9
+ export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.7.0';
10
10
 
11
11
  const VERB_ALIASES: Record<Verb, string[]> = {
12
12
  up: ['docker:up', 'docker-up', 'docker:up:detached', 'start', 'dev', 'serve'],