tonedial 0.1.2 → 0.1.4
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.js +22 -8
- package/dist/commands/doctor.js +27 -4
- package/dist/commands/get.js +41 -0
- package/dist/commands/rebuild.js +37 -16
- package/dist/commands/save.js +1 -1
- package/dist/commands/set.js +12 -26
- package/dist/commands/show.js +24 -19
- package/dist/core/anchors.js +1 -0
- package/dist/core/compile.js +2 -2
- package/dist/core/config.js +20 -8
- package/dist/core/i18n.js +32 -18
- package/dist/core/legacy-tone.js +6 -4
- package/dist/core/locales/es.js +16 -9
- package/dist/core/locales/ja.js +16 -9
- package/dist/core/locales/zh.js +16 -9
- package/dist/core/presets.js +3 -0
- package/dist/core/seeds.js +3 -3
- package/dist/core/selfupdate.js +25 -4
- package/dist/core/skill.js +33 -12
- package/dist/wiring/apply.js +55 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// Argument parsing, dispatch, and human/JSON output.
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { isAbsolute } from "node:path";
|
|
4
|
-
import { ConfigError, isConfigV2, nearest } from "./core/config.js";
|
|
4
|
+
import { ConfigError, SETTING_KEYS, isConfigV2, nearest } from "./core/config.js";
|
|
5
|
+
import { AXIS_ORDER } from "./core/anchors.js";
|
|
5
6
|
import { compile } from "./core/compile.js";
|
|
6
7
|
import { toCompileInput } from "./core/resolve.js";
|
|
7
8
|
import { inspectContextLayout } from "./core/seeds.js";
|
|
8
9
|
import { OperationError } from "./core/operations.js";
|
|
9
10
|
import { RegionError } from "./wiring/region.js";
|
|
10
11
|
import { connectionScopeFlag, installShape, resolveAgentPaths, resolvePaths } from "./core/paths.js";
|
|
11
|
-
import { messages } from "./core/i18n.js";
|
|
12
|
+
import { catalogue, configLanguage, detectLanguage, messages } from "./core/i18n.js";
|
|
12
13
|
import { isColourMode, palette } from "./ui/style.js";
|
|
13
14
|
import { cells, maxCells, resolveBudget, wrap } from "./ui/width.js";
|
|
14
15
|
import { interactive } from "./ui/prompt.js";
|
|
@@ -22,6 +23,7 @@ import { runSave } from "./commands/save.js";
|
|
|
22
23
|
import { runCreate } from "./commands/create.js";
|
|
23
24
|
import { runAddress, runPersona, runSet, runReset } from "./commands/set.js";
|
|
24
25
|
import { runShow } from "./commands/show.js";
|
|
26
|
+
import { runGet, settingHelp } from "./commands/get.js";
|
|
25
27
|
import { runUse } from "./commands/use.js";
|
|
26
28
|
import { runDoctor } from "./commands/doctor.js";
|
|
27
29
|
import { runEject } from "./commands/eject.js";
|
|
@@ -34,7 +36,7 @@ export const JSON_SCHEMA = 3;
|
|
|
34
36
|
* run time is the one thing that breaks all three binary packagers at once. A test asserts this
|
|
35
37
|
* equals the manifest, which is the cheap half of the trade.
|
|
36
38
|
*/
|
|
37
|
-
export const VERSION = "0.1.
|
|
39
|
+
export const VERSION = "0.1.4";
|
|
38
40
|
const FLAG_NAMES = ["project", "check", "explain", "global", "here", "migrateLegacy", "auto", "dryRun", "fix", "rollback", "fromOperation", "fromPreset", "restorePreset", "preset", "slug", "acceptUsage", "lang", "commands", "tools"];
|
|
39
41
|
/** Command-line spelling for each flag, where it differs from the field name. */
|
|
40
42
|
const FLAG_SPELLING = {
|
|
@@ -47,6 +49,7 @@ const FLAG_SPELLING = {
|
|
|
47
49
|
/** Each command declares its accepted flags and positional limit; null means unlimited. */
|
|
48
50
|
const COMMANDS = {
|
|
49
51
|
set: { run: runSet, flags: [], max: null, context: "state" },
|
|
52
|
+
get: { run: runGet, flags: [], max: 1 },
|
|
50
53
|
test: { run: runTest, flags: ["project", "dryRun", "preset", "acceptUsage"], max: 1 },
|
|
51
54
|
address: { run: runAddress, flags: [], max: 2, context: "state" },
|
|
52
55
|
persona: { run: runPersona, flags: [], max: 2, context: "state" },
|
|
@@ -77,7 +80,8 @@ export const COMMAND_NAMES = Object.keys(COMMANDS);
|
|
|
77
80
|
/** Intent groups share one command inventory between compact and full help. */
|
|
78
81
|
const COMMAND_GROUPS = [
|
|
79
82
|
["tone", [
|
|
80
|
-
["set", "set <
|
|
83
|
+
["set", "set <key>=<value> …"],
|
|
84
|
+
["get", "get [key]"],
|
|
81
85
|
["address", "address [self user]"],
|
|
82
86
|
["persona", "persona [agent user]"],
|
|
83
87
|
["reset", "reset [axis]"],
|
|
@@ -108,6 +112,7 @@ const COMMAND_GROUPS = [
|
|
|
108
112
|
const COMMAND_SYNTAX = COMMAND_GROUPS.flatMap(([, rows]) => rows);
|
|
109
113
|
const COMMAND_EXAMPLES = {
|
|
110
114
|
set: ["tonedial set", "tonedial set warmth=5 critique=4", "tonedial set persona.agent=reviewer"],
|
|
115
|
+
get: ["tonedial get", "tonedial get language", "tonedial get speech_level --json"],
|
|
111
116
|
test: ["tonedial test codex --dry-run", "tonedial test claude --project .", "tonedial test codex --preset my-tone --accept-usage"],
|
|
112
117
|
address: ['tonedial address "assistant" "friend"'],
|
|
113
118
|
persona: ["tonedial persona", 'tonedial persona "Fictional reviewer" "Developer"'],
|
|
@@ -216,7 +221,8 @@ function compactUsage(t, ui) {
|
|
|
216
221
|
].join("\n");
|
|
217
222
|
}
|
|
218
223
|
/** One command's syntax and only the flags that can affect that command. */
|
|
219
|
-
function commandUsage(name,
|
|
224
|
+
function commandUsage(name, copy, ui) {
|
|
225
|
+
const t = copy.cli;
|
|
220
226
|
const command = COMMANDS[name];
|
|
221
227
|
const syntax = COMMAND_SYNTAX.find(([key]) => key === name)?.[1] ?? name;
|
|
222
228
|
const flagKeys = [
|
|
@@ -231,6 +237,7 @@ function commandUsage(name, t, ui) {
|
|
|
231
237
|
return [syntax, t.help.flags[key]];
|
|
232
238
|
});
|
|
233
239
|
const width = Math.max(...flags.map(([flag]) => cells(flag)));
|
|
240
|
+
const keyWidth = Math.max(...[...AXIS_ORDER, ...SETTING_KEYS].map(key => maxCells(key)));
|
|
234
241
|
return [
|
|
235
242
|
ui.bold(`tonedial ${syntax}`),
|
|
236
243
|
"",
|
|
@@ -241,6 +248,12 @@ function commandUsage(name, t, ui) {
|
|
|
241
248
|
"",
|
|
242
249
|
ui.bold(t.help.examples),
|
|
243
250
|
...COMMAND_EXAMPLES[name].map(example => ` ${ui.accent(example)}`),
|
|
251
|
+
...(["get", "set"].includes(name) ? [
|
|
252
|
+
"", ui.bold(t.artifact.whatEachAxisObserves),
|
|
253
|
+
...helpRows(AXIS_ORDER.map(key => [key, settingHelp(key, copy).join(" · ")]), keyWidth, ui),
|
|
254
|
+
"", ui.bold(t.artifact.whatElseIsSettable),
|
|
255
|
+
...helpRows(SETTING_KEYS.map(key => [key, settingHelp(key, copy).join(" · ")]), keyWidth, ui),
|
|
256
|
+
] : []),
|
|
244
257
|
].join("\n");
|
|
245
258
|
}
|
|
246
259
|
/**
|
|
@@ -319,7 +332,8 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
319
332
|
// Resolved before the mode is validated, so the message about a bad `--color` is itself
|
|
320
333
|
// painted the way the rest of this invocation would have been.
|
|
321
334
|
const configPath = resolvePaths(env).config;
|
|
322
|
-
const
|
|
335
|
+
const copy = catalogue(configLanguage(configPath) ?? detectLanguage(env));
|
|
336
|
+
const say = copy.cli;
|
|
323
337
|
const colour = String(parsed.values.color);
|
|
324
338
|
const mode = isColourMode(colour) ? colour : "auto";
|
|
325
339
|
// The line budget is settled here beside the colour, because it is the same kind of decision:
|
|
@@ -353,7 +367,7 @@ export async function run(argv, env = process.env, cwd = process.cwd(), isTTY =
|
|
|
353
367
|
if (!Object.hasOwn(COMMANDS, name)) {
|
|
354
368
|
return fail(asJson, say.errors.notACommand(asJson ? name : humanText(name), COMMAND_NAMES, nearest(name, COMMAND_NAMES)), 2, uiErr);
|
|
355
369
|
}
|
|
356
|
-
process.stdout.write(`${commandUsage(name,
|
|
370
|
+
process.stdout.write(`${commandUsage(name, copy, ui)}\n`);
|
|
357
371
|
return 0;
|
|
358
372
|
}
|
|
359
373
|
// npx supplies no durable bare command for generated controls. Help and version stay available.
|
|
@@ -503,7 +517,7 @@ function fail(asJson, message, code, ui) {
|
|
|
503
517
|
}
|
|
504
518
|
else {
|
|
505
519
|
// A standalone path is a copy target, not prose to split into unusable fragments.
|
|
506
|
-
const lines = message.split("\n").flatMap((line) => isAbsolute(line) || /^(?:tonedial|npm|
|
|
520
|
+
const lines = message.split("\n").flatMap((line) => isAbsolute(line) || /^(?:tonedial|curl) |^npm (?:install|uninstall|update) -|^https?:\/\//.test(line) ? [humanText(line)] : wrap(humanText(line), ui.width));
|
|
507
521
|
process.stderr.write(lines.map((line) => ui.error(line)).join("\n") + "\n");
|
|
508
522
|
}
|
|
509
523
|
return code;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -515,11 +515,34 @@ async function doctorReport(ctx, showRollbackCommands = true, showCodexAction =
|
|
|
515
515
|
operations: inspected.journals.map(({ operationId, state }) => ({ operationId, state })),
|
|
516
516
|
};
|
|
517
517
|
if (inspected.lock.state !== "none" || inspected.journals.some(({ state }) => state === "pending" || state === "invalid")) {
|
|
518
|
+
const owner = inspected.lock.owner;
|
|
519
|
+
const blocking = inspected.journals.filter(({ state }) => state === "pending" || state === "invalid");
|
|
520
|
+
const shown = ctx.flags.verbose ? inspected.journals : inspected.journals.filter(({ operationId, state }) => state === "pending" || state === "invalid" || operationId === owner?.operationId);
|
|
521
|
+
const lines = [
|
|
522
|
+
...wrap(ctx.t.doctor.recoveryPending, ctx.ui.width - 2).map(line => ` ${ctx.ui.warn(line)}`),
|
|
523
|
+
...shown.map(({ operationId, state }) => ` ${operationId} ${ctx.t.doctor.operationState[state]}`),
|
|
524
|
+
];
|
|
525
|
+
if (owner !== null && !shown.some(({ operationId }) => operationId === owner.operationId))
|
|
526
|
+
lines.push(` ${owner.operationId}`);
|
|
527
|
+
if (inspected.lock.state === "live" || inspected.lock.state === "unknown" || inspected.lock.state === "invalid") {
|
|
528
|
+
lines.push(...wrap(ctx.t.doctor.rollbackStatus[inspected.lock.state === "live" ? "busy" : "unresolved"], ctx.ui.width - 2)
|
|
529
|
+
.map(line => ` ${line}`));
|
|
530
|
+
}
|
|
531
|
+
else if (inspected.lock.state === "dead" && owner?.kind === "mutation") {
|
|
532
|
+
const entry = inspected.journals.find(({ operationId }) => operationId === owner.operationId);
|
|
533
|
+
if (entry?.journal && entry.journal.owner.nonce === owner.nonce) {
|
|
534
|
+
const selection = inspectRollback(ctx.paths, owner.operationId);
|
|
535
|
+
if (entry.state === "pending" && !selection.synthetic && blocking.every(({ operationId, journal }) => operationId === owner.operationId && journal !== null)) {
|
|
536
|
+
lines.push("", ` ${ctx.t.doctor.resumeOperation}`, ...nextStep(repairCommand(ctx, `doctor --fix --from-operation ${owner.operationId}`), ctx.ui).slice(1));
|
|
537
|
+
}
|
|
538
|
+
if (showRollbackCommands && previewRollback(ctx.paths, owner.operationId, ctx.t).status === "ready") {
|
|
539
|
+
lines.push("", ...receiptLines(ctx.t.recovery.rollback(owner.operationId), ctx.ui.width - 2).map(line => ` ${line}`));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
lines.push(...nextStep(`${repairCommand(ctx, "doctor")} --json`, ctx.ui));
|
|
518
544
|
return {
|
|
519
|
-
lines
|
|
520
|
-
...wrap(ctx.t.doctor.recoveryPending, ctx.ui.width - 2).map((line) => ` ${ctx.ui.warn(line)}`),
|
|
521
|
-
...recovery.operations.map(({ operationId, state }) => ` ${operationId} ${ctx.t.doctor.operationState[state]}`),
|
|
522
|
-
],
|
|
545
|
+
lines,
|
|
523
546
|
// The damaged config is deliberately not parsed here. `operation` is the only safe context
|
|
524
547
|
// fact to expose until the owner-selected rollback or recovery has completed.
|
|
525
548
|
json: {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Read active settings without compiling, repairing, or interpreting stored text.
|
|
2
|
+
import { isAxis, AGE_BANDS, GENDERS, ConfigError, nearest, settingValues } from "../core/config.js";
|
|
3
|
+
import { LANGUAGES, catalogue, detectLanguage, isLanguage } from "../core/i18n.js";
|
|
4
|
+
import { humanText } from "../ui/format.js";
|
|
5
|
+
import { maxCells, padEnd, wrap } from "../ui/width.js";
|
|
6
|
+
import { planConfig } from "./compile.js";
|
|
7
|
+
/** The same descriptions and allowed tokens serve key reads and command help. */
|
|
8
|
+
export function settingHelp(key, copy) {
|
|
9
|
+
if (isAxis(key)) {
|
|
10
|
+
const hint = copy.axisHints[key];
|
|
11
|
+
return [hint.owns, `1–5 · 1 ${hint.low} · 5 ${hint.high}`];
|
|
12
|
+
}
|
|
13
|
+
const choices = key === "language" ? LANGUAGES : key === "persona.age" ? AGE_BANDS : key === "persona.gender" ? GENDERS : null;
|
|
14
|
+
return [copy.settingHints[key], ...(choices ? [choices.join(" | ")] : [])];
|
|
15
|
+
}
|
|
16
|
+
export function runGet(ctx, args) {
|
|
17
|
+
const { change, config } = planConfig(ctx);
|
|
18
|
+
if (change.before === null)
|
|
19
|
+
throw new ConfigError(ctx.t.test.missingConfig);
|
|
20
|
+
const all = settingValues(config);
|
|
21
|
+
const [requested] = args;
|
|
22
|
+
if (requested !== undefined && !Object.hasOwn(all, requested)) {
|
|
23
|
+
throw new ConfigError(ctx.t.axis.notSettable(ctx.asJson ? requested : humanText(requested), nearest(requested, Object.keys(all))));
|
|
24
|
+
}
|
|
25
|
+
const values = requested === undefined ? all : { [requested]: all[requested] };
|
|
26
|
+
const column = Math.max(...Object.keys(values).map(key => maxCells(key)));
|
|
27
|
+
const budget = ctx.ui.width - column - 4;
|
|
28
|
+
const lines = Object.entries(values).flatMap(([key, value]) => {
|
|
29
|
+
const text = value === null ? "—" : humanText(String(value));
|
|
30
|
+
if (budget < 12)
|
|
31
|
+
return [` ${ctx.ui.accent(key)}`, ...wrap(text, ctx.ui.width - 4).map(line => ` ${line}`)];
|
|
32
|
+
const [first, ...rest] = wrap(text, budget);
|
|
33
|
+
return [` ${ctx.ui.accent(padEnd(key, column))} ${first ?? ""}`,
|
|
34
|
+
...rest.map(line => `${" ".repeat(column + 4)}${line}`)];
|
|
35
|
+
});
|
|
36
|
+
if (requested !== undefined) {
|
|
37
|
+
const copy = catalogue(isLanguage(config.language) ? config.language : detectLanguage(ctx.env));
|
|
38
|
+
lines.push("", ...settingHelp(requested, copy).flatMap(hint => wrap(hint, ctx.ui.width - 2).map(line => ` ${ctx.ui.dim(line)}`)));
|
|
39
|
+
}
|
|
40
|
+
return { lines, json: { values } };
|
|
41
|
+
}
|
package/dist/commands/rebuild.js
CHANGED
|
@@ -8,7 +8,7 @@ import { compile } from "../core/compile.js";
|
|
|
8
8
|
import { toCompileInput } from "../core/resolve.js";
|
|
9
9
|
import { isLanguage, LANGUAGES } from "../core/i18n.js";
|
|
10
10
|
import { recoverLegacyTone } from "../core/legacy-tone.js";
|
|
11
|
-
import { exportAliasesState, stateOverlapsInstructions } from "../core/paths.js";
|
|
11
|
+
import { connectionScopeFlag, exportAliasesState, stateOverlapsInstructions } from "../core/paths.js";
|
|
12
12
|
import { applyPreset, matchesPreset, assertPresetSelection, BUNDLED, customPresetDir, inspectPresetLibrary, parsePreset, PRESET_MAX_BYTES, resolvePreset, serializePreset, SLUG_PATTERN, } from "../core/presets.js";
|
|
13
13
|
import { contextArtifactTargets, generatedSeedSelection, planGeneratedSeedRebuild, seedFiles, SEED_VERSION, } from "../core/seeds.js";
|
|
14
14
|
import { inspectOperations, readJournal, retireOperationForAppRebuild, } from "../core/operations.js";
|
|
@@ -254,9 +254,14 @@ export async function runAppRebuild(ctx) {
|
|
|
254
254
|
else if (!ctx.flags.auto && ctx.canPrompt) {
|
|
255
255
|
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
256
256
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
const operation = t.recovery.resumeOperation(owner.operationId).split("\n").flatMap(line => line === owner.operationId ? [line] : wrap(line, ctx.ui.width - 2));
|
|
258
|
+
process.stdout.write(`${operation.map(line => ` ${line}`).join("\n")}\n`);
|
|
259
|
+
const question = wrap(t.recovery.resumeQuestion, ctx.ui.width - 3).map(line => ` ${line}`).join("\n");
|
|
260
|
+
const answer = (await input.question(`${question} `)).trim();
|
|
261
|
+
if (answer === "")
|
|
262
|
+
throw new ConfigError(t.recovery.cancelled);
|
|
263
|
+
if (answer !== owner.operationId)
|
|
264
|
+
throw new ConfigError(t.recovery.operationMismatch);
|
|
260
265
|
retirementId = owner.operationId;
|
|
261
266
|
}
|
|
262
267
|
finally {
|
|
@@ -394,6 +399,8 @@ export async function runAppRebuild(ctx) {
|
|
|
394
399
|
}
|
|
395
400
|
const seedsSelection = generatedSeedSelection(ctx.paths, config, ctx.flags.tools !== null && ctx.flags.tools !== "none", t);
|
|
396
401
|
const seeds = planGeneratedSeedRebuild(ctx.paths, config.managedSeeds, seedsSelection, t);
|
|
402
|
+
const generatedSeeds = seedFiles(ctx.paths, seedsSelection);
|
|
403
|
+
const rewrittenGenerated = new Set(generatedSeeds.filter(seed => seeds.operationActions.some(({ action }) => action.path === seed.path || (object(action.after)?.type === "directory" && seed.path.startsWith(`${action.path}${sep}`)))).map(seed => seed.path));
|
|
397
404
|
checks.push(...seeds.checks);
|
|
398
405
|
config.managedSeeds = seeds.records;
|
|
399
406
|
config.seedVersion = SEED_VERSION;
|
|
@@ -457,40 +464,54 @@ export async function runAppRebuild(ctx) {
|
|
|
457
464
|
auto: ctx.flags.auto, dryRun: ctx.flags.dryRun, verbose: ctx.flags.verbose, ui: ctx.ui, t,
|
|
458
465
|
heading: t.recovery.heading,
|
|
459
466
|
interactive: () => ctx.canPrompt, ...(ctx.canPrompt ? {} : { out: () => { } }),
|
|
460
|
-
recoveryPresentation: { lines: presentation,
|
|
467
|
+
recoveryPresentation: { lines: presentation,
|
|
468
|
+
failureMessage: (id, phase, path, code) => `${t.recovery.failure(id, phase, path, code)}\ntonedial doctor ${connectionScopeFlag(ctx.agent)} --json` },
|
|
461
469
|
verify: () => {
|
|
470
|
+
let verificationPath = ctx.paths.config;
|
|
471
|
+
const mismatch = () => new ConfigError(t.recovery.sourceInvalid, { cause: { code: "VERIFY_MISMATCH", path: verificationPath } });
|
|
462
472
|
try {
|
|
463
473
|
const repaired = parseConfig(capturedText(captureRawEntry(ctx.paths.config, t, { within: ctx.paths.root })) ?? "", t);
|
|
464
474
|
if (!isConfigV2(repaired) || serializeConfig(repaired, t) !== serializeConfig(config, t))
|
|
465
|
-
throw
|
|
466
|
-
for (const seed of
|
|
475
|
+
throw mismatch();
|
|
476
|
+
for (const seed of generatedSeeds) {
|
|
477
|
+
verificationPath = seed.path;
|
|
467
478
|
const now = captureRawEntry(seed.path, t, { within: ctx.paths.root });
|
|
468
479
|
if (capturedText(now) !== seed.contents)
|
|
469
|
-
throw
|
|
480
|
+
throw mismatch();
|
|
470
481
|
}
|
|
471
|
-
for (const target of artifacts)
|
|
482
|
+
for (const target of artifacts) {
|
|
483
|
+
verificationPath = target.path;
|
|
472
484
|
if (capturedText(captureRawEntry(target.path, t, { within: ctx.paths.root })) !== artifact)
|
|
473
|
-
throw
|
|
485
|
+
throw mismatch();
|
|
486
|
+
}
|
|
487
|
+
verificationPath = ctx.paths.tone;
|
|
474
488
|
if (capturedText(captureRawEntry(ctx.paths.tone, t, { within: ctx.paths.root })) !== artifact)
|
|
475
|
-
throw
|
|
489
|
+
throw mismatch();
|
|
476
490
|
for (const entry of library.entries) {
|
|
477
491
|
for (const before of entry.selectionReads) {
|
|
478
|
-
|
|
492
|
+
// Rewritten generated entries were checked against their new bytes above.
|
|
493
|
+
if (before.path === restoredPreset?.capture.path || rewrittenGenerated.has(before.path))
|
|
479
494
|
continue;
|
|
495
|
+
verificationPath = before.path;
|
|
480
496
|
const now = captureRawEntry(before.path, t, { within: ctx.paths.root, maxBytes: PRESET_MAX_BYTES });
|
|
481
497
|
if (now.type !== before.type || now.identity !== before.identity || now.mode !== before.mode ||
|
|
482
498
|
(now.bytes === null ? before.bytes !== null : before.bytes === null || !now.bytes.equals(before.bytes))) {
|
|
483
|
-
throw
|
|
499
|
+
throw mismatch();
|
|
484
500
|
}
|
|
485
501
|
}
|
|
486
502
|
}
|
|
487
503
|
for (const slug of [...copies, ...(restoredPreset ? [restoredPreset.slug] : [])]) {
|
|
504
|
+
verificationPath = slug === restoredPreset?.slug ? restoredPreset.capture.path
|
|
505
|
+
: library.recoveryCopies.find(copy => copy.preset.slug === slug).path;
|
|
488
506
|
if (resolvePreset(ctx.paths.presets, slug, t) === null)
|
|
489
|
-
throw
|
|
507
|
+
throw mismatch();
|
|
490
508
|
}
|
|
491
509
|
}
|
|
492
|
-
catch {
|
|
493
|
-
|
|
510
|
+
catch (cause) {
|
|
511
|
+
if (cause instanceof ConfigError && cause.cause?.code === "VERIFY_MISMATCH")
|
|
512
|
+
throw cause;
|
|
513
|
+
const code = cause !== null && typeof cause === "object" && "code" in cause ? cause.code : undefined;
|
|
514
|
+
throw new ConfigError(t.recovery.sourceInvalid, { cause: { code, path: verificationPath, cause } });
|
|
494
515
|
}
|
|
495
516
|
},
|
|
496
517
|
};
|
package/dist/commands/save.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function savePreset(ctx, displayName, state, options = {}) {
|
|
|
60
60
|
[address?.self_ref ? ctx.t.persona.callsItself(humanText(address.self_ref)) : "",
|
|
61
61
|
address?.user_ref ? ctx.t.persona.callsYou(humanText(address.user_ref)) : ""].filter(Boolean).join(" · "),
|
|
62
62
|
...["user", "agent"].map((field) => `${ctx.t.persona.descriptionFull(field, humanText(config.persona[field] ?? ""))} (${ctx.t.create.descriptionCount([...(config.persona[field] ?? "")].length, PERSONA_DESCRIPTION_MAX)})`),
|
|
63
|
-
`${
|
|
63
|
+
`${config.safeword ? ctx.t.settings.safewordSet(humanText(config.safeword)) : ctx.t.settings.noSafeword} · ${ctx.t.settings.register(config.speech_level)}`,
|
|
64
64
|
];
|
|
65
65
|
for (const detail of details.filter(Boolean))
|
|
66
66
|
review.push(...wrap(detail, ctx.ui.width - 2).map((line) => ` ${line}`));
|
package/dist/commands/set.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// job is to say so. Every selected level remains unchanged, including contradictions.
|
|
6
6
|
import { AXIS_DEFAULT, AXIS_ORDER, riskyAxes } from "../core/anchors.js";
|
|
7
7
|
import { check as checkCombos, PERSONA_DESCRIPTION_MAX } from "../core/combos.js";
|
|
8
|
-
import { AGE_BANDS, GENDERS, SPEECH_LEVELS, ConfigError, SETTING_KEYS, isAxis, nearest, nearestAxis, serializeConfig, } from "../core/config.js";
|
|
8
|
+
import { AGE_BANDS, GENDERS, SPEECH_LEVELS, ConfigError, SETTING_KEYS, isAxis, nearest, nearestAxis, serializeConfig, settingValues, } from "../core/config.js";
|
|
9
9
|
import { toCompileInput } from "../core/resolve.js";
|
|
10
10
|
import { LANGUAGES, catalogue, isLanguage } from "../core/i18n.js";
|
|
11
11
|
import { presetSettings, resolvePreset } from "../core/presets.js";
|
|
@@ -157,33 +157,19 @@ function movementLines(moves, ctx, axes) {
|
|
|
157
157
|
* Returns `undefined` for a key this tool does not have, which `assign` then rejects by name.
|
|
158
158
|
*/
|
|
159
159
|
function readKey(config, key, t) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (key === "speech_level")
|
|
163
|
-
return config.speech_level;
|
|
164
|
-
if (key === "language")
|
|
165
|
-
return config.language;
|
|
166
|
-
if (key.startsWith("persona.")) {
|
|
167
|
-
const field = key.slice("persona.".length);
|
|
168
|
-
if (field === "age")
|
|
169
|
-
return config.persona.age ?? UNSET;
|
|
170
|
-
if (field === "gender")
|
|
171
|
-
return config.persona.gender ?? UNSET;
|
|
172
|
-
if (field === "self_ref")
|
|
173
|
-
return config.persona.address?.self_ref ?? UNSET;
|
|
174
|
-
if (field === "user_ref")
|
|
175
|
-
return config.persona.address?.user_ref ?? UNSET;
|
|
176
|
-
if (field === "note") {
|
|
177
|
-
const note = config.persona.note;
|
|
178
|
-
return note ? t.persona.noteChars([...note].length) : UNSET;
|
|
179
|
-
}
|
|
180
|
-
if (field === "user" || field === "agent") {
|
|
181
|
-
const description = config.persona[field];
|
|
182
|
-
return description ? t.persona.descriptionChars(field, [...description].length) : UNSET;
|
|
183
|
-
}
|
|
160
|
+
const values = settingValues(config);
|
|
161
|
+
if (!Object.hasOwn(values, key))
|
|
184
162
|
return undefined;
|
|
163
|
+
const value = values[key];
|
|
164
|
+
if (value === null)
|
|
165
|
+
return UNSET;
|
|
166
|
+
if (typeof value === "string") {
|
|
167
|
+
if (key === "persona.note")
|
|
168
|
+
return t.persona.noteChars([...value].length);
|
|
169
|
+
if (key === "persona.user" || key === "persona.agent")
|
|
170
|
+
return t.persona.descriptionChars(key.slice("persona.".length), [...value].length);
|
|
185
171
|
}
|
|
186
|
-
return
|
|
172
|
+
return String(value);
|
|
187
173
|
}
|
|
188
174
|
/**
|
|
189
175
|
* Assign each pair, and record the movement each one actually made.
|
package/dist/commands/show.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isAbsolute } from "node:path";
|
|
|
5
5
|
import { stripVTControlCharacters } from "node:util";
|
|
6
6
|
import { AXIS_DEFAULT, AXIS_GROUP, AXIS_KIND, AXIS_RISK_FROM, riskyAxes } from "../core/anchors.js";
|
|
7
7
|
import { check as checkCombos, PERSONA_DESCRIPTION_MAX } from "../core/combos.js";
|
|
8
|
-
import { ConfigError, isConfigV2 } from "../core/config.js";
|
|
8
|
+
import { ConfigError, isConfigV2, settingValues } from "../core/config.js";
|
|
9
9
|
import { compile, measure } from "../core/compile.js";
|
|
10
10
|
import { withPreset, matchesPreset, inspectPresetLibrary, resolvePreset } from "../core/presets.js";
|
|
11
11
|
import { toCompileInput } from "../core/resolve.js";
|
|
@@ -31,6 +31,7 @@ export function runShow(ctx, args) {
|
|
|
31
31
|
}
|
|
32
32
|
const shown = preview ? withPreset(config, preview) : config;
|
|
33
33
|
const axes = shown.axes;
|
|
34
|
+
const values = settingValues(shown);
|
|
34
35
|
const input = toCompileInput(shown);
|
|
35
36
|
const artifact = compile(input);
|
|
36
37
|
const size = measure(artifact);
|
|
@@ -63,33 +64,37 @@ export function runShow(ctx, args) {
|
|
|
63
64
|
for (const folded of wrap(sizeLabel, ui.width - 2))
|
|
64
65
|
lines.push(` ${ui.dim(folded)}`);
|
|
65
66
|
}
|
|
66
|
-
const
|
|
67
|
-
const personaSet = Boolean(
|
|
67
|
+
const { "persona.age": age, "persona.gender": gender, "persona.note": note, "persona.self_ref": selfRef, "persona.user_ref": userRef, "persona.user": user, "persona.agent": agent } = values;
|
|
68
|
+
const personaSet = Boolean(age || gender || note || selfRef || userRef || user || agent);
|
|
68
69
|
const gauges = [gaugeHeader(ui, ctx.t), ...gaugeBlock(axes, ui)];
|
|
69
70
|
const gaugeWidth = Math.max(...gauges.map((line) => maxCells(stripVTControlCharacters(line))));
|
|
70
71
|
const detailWidth = ui.terminal && ui.width >= 119 ? ui.width - gaugeWidth - 4 : ui.width - 2;
|
|
71
72
|
const details = [ui.bold(ctx.t.artifact.settingsHeading)];
|
|
72
73
|
if (personaSet) {
|
|
73
74
|
const bits = [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
age ? humanText(age) : null,
|
|
76
|
+
gender ? humanText(gender) : null,
|
|
77
|
+
selfRef ? ctx.t.persona.callsItself(humanText(selfRef)) : null,
|
|
78
|
+
userRef ? ctx.t.persona.callsYou(humanText(userRef)) : null,
|
|
79
|
+
note ? ctx.t.persona.noteChars([...note].length) : null,
|
|
80
|
+
user && agent ? ctx.t.persona.descriptionCounts([...user].length, [...agent].length, PERSONA_DESCRIPTION_MAX) : null,
|
|
81
|
+
user && !agent ? ctx.t.persona.descriptionChars("user", [...user].length) : null,
|
|
82
|
+
agent && !user ? ctx.t.persona.descriptionChars("agent", [...agent].length) : null,
|
|
82
83
|
].filter(Boolean);
|
|
83
84
|
if (bits.length)
|
|
84
85
|
for (const folded of wrap(bits.join(" · "), detailWidth))
|
|
85
86
|
details.push(ui.dim(folded));
|
|
86
|
-
for (const [field, value] of [["agent",
|
|
87
|
+
for (const [field, value] of [["agent", agent], ["user", user]]) {
|
|
87
88
|
if (value === null)
|
|
88
89
|
continue;
|
|
89
90
|
for (const folded of wrap(ctx.t.persona.descriptionFull(field, humanText(value)), detailWidth)) {
|
|
90
91
|
details.push(ui.dim(folded));
|
|
91
92
|
}
|
|
92
93
|
}
|
|
94
|
+
if (note !== null) {
|
|
95
|
+
for (const folded of wrap(ctx.t.persona.descriptionFull("note", humanText(note)), detailWidth))
|
|
96
|
+
details.push(ui.dim(folded));
|
|
97
|
+
}
|
|
93
98
|
}
|
|
94
99
|
// Folded rather than laid out: the safeword is user text of unbounded length, so this row
|
|
95
100
|
// can always exceed the budget and no amount of shortening the fixed words prevents it.
|
|
@@ -97,7 +102,7 @@ export function runShow(ctx, args) {
|
|
|
97
102
|
? [...new Set(Object.values(config.wiring.registrations).map((record) => `${record.target} (${record.scope})`))]
|
|
98
103
|
: Object.keys(config.wiring);
|
|
99
104
|
for (const text of [
|
|
100
|
-
stateRow(
|
|
105
|
+
stateRow(values, ctx.t), ctx.t.artifact.displaySettings(values.language),
|
|
101
106
|
ctx.t.artifact.connectionRecords(records.map(humanText)),
|
|
102
107
|
])
|
|
103
108
|
for (const folded of wrap(text, detailWidth))
|
|
@@ -170,10 +175,10 @@ export function runShow(ctx, args) {
|
|
|
170
175
|
riskAxes: riskyAxes(axes),
|
|
171
176
|
riskThresholds: AXIS_RISK_FROM,
|
|
172
177
|
persona: shown.persona,
|
|
173
|
-
safeword:
|
|
174
|
-
speech_level:
|
|
178
|
+
safeword: values.safeword,
|
|
179
|
+
speech_level: values.speech_level,
|
|
175
180
|
// Interface language never changes the compiled tone.
|
|
176
|
-
language:
|
|
181
|
+
language: values.language,
|
|
177
182
|
chars: size.chars,
|
|
178
183
|
artifact: { exists: artifactOnDisk },
|
|
179
184
|
},
|
|
@@ -197,9 +202,9 @@ function titleRow(name, sizeLabel, ctx) {
|
|
|
197
202
|
* Safeword and register on one row. Plain text, because it is folded before it is
|
|
198
203
|
* painted and an escape sequence measures as characters while occupying no cell.
|
|
199
204
|
*/
|
|
200
|
-
function stateRow(
|
|
201
|
-
const safeword =
|
|
202
|
-
return `${safeword} · ${t.settings.register(
|
|
205
|
+
function stateRow(values, t) {
|
|
206
|
+
const safeword = values.safeword ? t.settings.safewordSet(humanText(values.safeword)) : t.settings.noSafeword;
|
|
207
|
+
return `${safeword} · ${t.settings.register(values.speech_level)}`;
|
|
203
208
|
}
|
|
204
209
|
// Presence alone establishes neither current contents nor native loading.
|
|
205
210
|
function artifactExists(ctx) {
|
package/dist/core/anchors.js
CHANGED
|
@@ -164,6 +164,7 @@ export const SPEECH_LEVEL = {
|
|
|
164
164
|
auto: "In languages with honorifics, choose address forms for the current relationship and scene; keep them consistent within each reply.",
|
|
165
165
|
polite: "In languages with honorifics, use polite address forms even with familiar wording.",
|
|
166
166
|
plain: "In languages with honorifics, use plain address forms even with formal wording.",
|
|
167
|
+
rough: "In languages with honorifics, use rough, cheeky non-honorific sentence endings; keep the configured address terms and the selected formality, warmth, profanity and playfulness levels.",
|
|
167
168
|
};
|
|
168
169
|
export const CONTROLS = {
|
|
169
170
|
/**
|
package/dist/core/compile.js
CHANGED
|
@@ -35,8 +35,8 @@ function validate(config) {
|
|
|
35
35
|
throw new RangeError(`axis ${axis} is ${level}, not a level 1 to 5`);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
if (!(config.speech_level
|
|
39
|
-
throw new RangeError(`speech_level is ${config.speech_level}, not auto, polite, or
|
|
38
|
+
if (!Object.hasOwn(SPEECH_LEVEL, config.speech_level)) {
|
|
39
|
+
throw new RangeError(`speech_level is ${config.speech_level}, not auto, polite, plain, or rough`);
|
|
40
40
|
}
|
|
41
41
|
if (config.persona !== null) {
|
|
42
42
|
for (const field of ["user", "agent"]) {
|
package/dist/core/config.js
CHANGED
|
@@ -10,7 +10,7 @@ export const CONFIG_SCHEMA = 2;
|
|
|
10
10
|
export const LEGACY_CONFIG_SCHEMA = 1;
|
|
11
11
|
export const AGE_BANDS = ["unspecified", "teens", "20s", "30s", "40s", "50s+"];
|
|
12
12
|
export const GENDERS = ["unspecified", "masculine", "feminine", "neutral"];
|
|
13
|
-
export const SPEECH_LEVELS = ["auto", "polite", "plain"];
|
|
13
|
+
export const SPEECH_LEVELS = ["auto", "polite", "plain", "rough"];
|
|
14
14
|
export const WIRING_SCOPES = ["global", "project"];
|
|
15
15
|
export const RESOURCE_KINDS = ["hook", "skill", "shell", "reader", "command"];
|
|
16
16
|
export const RESOURCE_TYPES = ["file", "directory", "symlink"];
|
|
@@ -853,20 +853,32 @@ export const isAxis = (name) => AXIS_ORDER.includes(name);
|
|
|
853
853
|
/**
|
|
854
854
|
* Everything `set <key>=<value>` takes that is not an axis.
|
|
855
855
|
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
* carries its own parsing and its own refusal.
|
|
856
|
+
* Shared by key discovery, setting reads, and the did-you-mean match in `set`.
|
|
857
|
+
* Assignment retains its own validation.
|
|
859
858
|
*
|
|
860
|
-
*
|
|
861
|
-
* test that drives every entry through `set`. A branch added to `assign` and never listed here
|
|
862
|
-
* is invisible: it would not appear in `--explain` and `nearest` would not suggest it, and
|
|
863
|
-
* nothing fails. Closing that needs `assign` to be a table rather than a chain of `if`s.
|
|
859
|
+
* Command tests exercise this inventory through both setting and reading values.
|
|
864
860
|
*/
|
|
865
861
|
export const SETTING_KEYS = [
|
|
866
862
|
"safeword", "speech_level", "language",
|
|
867
863
|
"persona.age", "persona.gender", "persona.self_ref", "persona.user_ref", "persona.note",
|
|
868
864
|
"persona.user", "persona.agent",
|
|
869
865
|
];
|
|
866
|
+
/** Current editable values, without presentation labels or internal ownership metadata. */
|
|
867
|
+
export function settingValues(config) {
|
|
868
|
+
return {
|
|
869
|
+
...Object.fromEntries(AXIS_ORDER.map(axis => [axis, config.axes[axis]])),
|
|
870
|
+
safeword: config.safeword,
|
|
871
|
+
speech_level: config.speech_level,
|
|
872
|
+
language: config.language,
|
|
873
|
+
"persona.age": config.persona.age,
|
|
874
|
+
"persona.gender": config.persona.gender,
|
|
875
|
+
"persona.self_ref": config.persona.address?.self_ref ?? null,
|
|
876
|
+
"persona.user_ref": config.persona.address?.user_ref ?? null,
|
|
877
|
+
"persona.note": config.persona.note,
|
|
878
|
+
"persona.user": config.persona.user,
|
|
879
|
+
"persona.agent": config.persona.agent,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
870
882
|
/** Levenshtein-lite: enough to say "did you mean critique?" and no more. */
|
|
871
883
|
export function nearest(name, candidates) {
|
|
872
884
|
const distance = (a, b) => {
|