@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.
- package/README.md +11 -3
- package/package.json +1 -1
- package/rubric/vdx-rubric.yaml +4 -3
- package/src/ai.ts +291 -46
- package/src/conversations.ts +11 -2
- package/src/doctor.ts +88 -5
- package/src/hosting.ts +28 -5
- package/src/index.ts +107 -11
- package/src/init.ts +1 -1
- package/src/memory.ts +367 -0
- package/src/publish.ts +11 -0
package/src/conversations.ts
CHANGED
|
@@ -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))
|
|
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 {
|
|
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 = [
|
|
515
|
-
(
|
|
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) =>
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
|
208
|
-
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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'],
|