tonedial 0.1.1 → 0.1.3
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/create.js +7 -7
- package/dist/commands/doctor.js +21 -14
- package/dist/commands/edit.js +3 -3
- package/dist/commands/get.js +41 -0
- package/dist/commands/init.js +6 -12
- package/dist/commands/save.js +3 -11
- package/dist/commands/set.js +28 -79
- package/dist/commands/show.js +25 -24
- package/dist/commands/transfer.js +0 -1
- package/dist/commands/update.js +89 -42
- package/dist/commands/use.js +3 -4
- package/dist/commands/wire.js +1 -1
- package/dist/core/anchors.js +1 -0
- package/dist/core/combos.js +6 -38
- package/dist/core/compile.js +2 -2
- package/dist/core/config.js +20 -9
- package/dist/core/i18n.js +76 -74
- package/dist/core/legacy-tone.js +6 -4
- package/dist/core/locales/es.js +38 -37
- package/dist/core/locales/ja.js +38 -37
- package/dist/core/locales/zh.js +38 -37
- package/dist/core/seeds.js +3 -3
- package/dist/core/selfupdate.js +69 -3
- package/dist/core/skill.js +28 -16
- package/dist/ui/format.js +3 -6
- package/package.json +1 -1
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))
|
|
@@ -148,13 +153,9 @@ export function runShow(ctx, args) {
|
|
|
148
153
|
const [first, ...rest] = wrap(legend, ui.width - 4);
|
|
149
154
|
lines.push(` ${ui.accent("*")} ${ui.dim(first)}`, ...rest.map((line) => ` ${ui.dim(line)}`), ...nextStep(preview ? `tonedial use ${preview.slug}` : needsSave ? saveCommand : "tonedial show --explain", ui).slice(1));
|
|
150
155
|
}
|
|
151
|
-
const combinations = checkCombos(axes
|
|
156
|
+
const combinations = checkCombos(axes);
|
|
152
157
|
const warnings = combinations.map((w) => combination(w, ctx.t));
|
|
153
|
-
if (personaSet) {
|
|
154
|
-
warnings.push(ctx.t.notice.personaUnanchored);
|
|
155
|
-
}
|
|
156
158
|
warnings.push(...riskWarnings(axes, ctx.t));
|
|
157
|
-
warnings.push(ctx.t.notice.modelExpectation);
|
|
158
159
|
if (activeConflict && preview === null)
|
|
159
160
|
warnings.push(ctx.t.preset.activeConflict(config.preset));
|
|
160
161
|
return {
|
|
@@ -174,10 +175,10 @@ export function runShow(ctx, args) {
|
|
|
174
175
|
riskAxes: riskyAxes(axes),
|
|
175
176
|
riskThresholds: AXIS_RISK_FROM,
|
|
176
177
|
persona: shown.persona,
|
|
177
|
-
safeword:
|
|
178
|
-
speech_level:
|
|
178
|
+
safeword: values.safeword,
|
|
179
|
+
speech_level: values.speech_level,
|
|
179
180
|
// Interface language never changes the compiled tone.
|
|
180
|
-
language:
|
|
181
|
+
language: values.language,
|
|
181
182
|
chars: size.chars,
|
|
182
183
|
artifact: { exists: artifactOnDisk },
|
|
183
184
|
},
|
|
@@ -201,9 +202,9 @@ function titleRow(name, sizeLabel, ctx) {
|
|
|
201
202
|
* Safeword and register on one row. Plain text, because it is folded before it is
|
|
202
203
|
* painted and an escape sequence measures as characters while occupying no cell.
|
|
203
204
|
*/
|
|
204
|
-
function stateRow(
|
|
205
|
-
const safeword =
|
|
206
|
-
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)}`;
|
|
207
208
|
}
|
|
208
209
|
// Presence alone establishes neither current contents nor native loading.
|
|
209
210
|
function artifactExists(ctx) {
|
package/dist/commands/update.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// Update
|
|
1
|
+
// Update through the installation owner and report application-state work separately.
|
|
2
2
|
import { VERSION } from "../cli.js";
|
|
3
3
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
4
4
|
import { TextDecoder } from "node:util";
|
|
5
5
|
import { ConfigError, parseConfig } from "../core/config.js";
|
|
6
|
-
import { AGENT_TOOLS, UPDATE_COMMAND, installShape } from "../core/paths.js";
|
|
7
|
-
import { REGISTRY, downloadRelease, haveTar, installRelease, installedVersion, latestRelease } from "../core/selfupdate.js";
|
|
6
|
+
import { AGENT_TOOLS, UPDATE_COMMAND, installShape, shellQuote } from "../core/paths.js";
|
|
7
|
+
import { REGISTRY, downloadRelease, haveTar, inspectNpmInstall, installRelease, installedVersion, latestRelease, updateNpmInstall } from "../core/selfupdate.js";
|
|
8
8
|
import { confirm } from "../ui/prompt.js";
|
|
9
9
|
import { SEED_VERSION, inspectSeeds, recordedSeedSelection } from "../core/seeds.js";
|
|
10
|
-
import { humanText } from "../ui/format.js";
|
|
10
|
+
import { humanText, receiptLines } from "../ui/format.js";
|
|
11
11
|
import { urlIdentity } from "../core/net.js";
|
|
12
12
|
/** A damaged state file must not disable package guidance or reveal its parser excerpt. */
|
|
13
13
|
function updateConfig(ctx) {
|
|
@@ -84,16 +84,40 @@ export async function runUpdate(ctx) {
|
|
|
84
84
|
stateIssue = "unreadable";
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
const lines = [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
const lines = [` ${ctx.ui.bold(ctx.t.update.running(VERSION))}`, ""];
|
|
88
|
+
const preview = () => {
|
|
89
|
+
if (!ctx.asJson) {
|
|
90
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
91
|
+
lines.length = 0;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
94
|
let replaced = null;
|
|
95
|
+
let installed = null;
|
|
96
|
+
let outcome;
|
|
95
97
|
if (shape !== "curl") {
|
|
96
|
-
|
|
98
|
+
if (ctx.flags.dryRun) {
|
|
99
|
+
outcome = "dry-run";
|
|
100
|
+
lines.push(` ${ctx.t.update.toUpdate}`, ` ${ctx.ui.accent(command)}`, ` ${ctx.t.wiring.dryRun}`);
|
|
101
|
+
}
|
|
102
|
+
else if (!ctx.flags.auto && !ctx.canPrompt) {
|
|
103
|
+
outcome = "approval-required";
|
|
104
|
+
lines.push(` ${ctx.t.update.toUpdate}`, ` ${ctx.ui.accent(command)}`, ...receiptLines(ctx.t.update.needsApproval, ctx.ui.width - 2).map(line => ` ${line}`));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const target = inspectNpmInstall(ctx.env, ctx.t);
|
|
108
|
+
lines.push(` ${ctx.t.update.toUpdate}`, ` ${ctx.ui.accent(`${command} --prefix "${shellQuote(humanText(target.prefix))}"`)}`);
|
|
109
|
+
preview();
|
|
110
|
+
if (ctx.flags.auto || (await confirm(ctx.t.update.npmQuestion)) === "yes") {
|
|
111
|
+
installed = updateNpmInstall(target, ctx.env, ctx.t);
|
|
112
|
+
replaced = installed === target.version ? null : installed;
|
|
113
|
+
outcome = replaced === null ? "unchanged" : "updated";
|
|
114
|
+
lines.push(` ${ctx.ui.success(ctx.t.update.npmComplete(installed))}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
outcome = "declined";
|
|
118
|
+
lines.push(` ${ctx.t.update.declined}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
97
121
|
}
|
|
98
122
|
else {
|
|
99
123
|
// Before the network, like `install.sh` checks its tools before it fetches anything.
|
|
@@ -104,29 +128,38 @@ export async function runUpdate(ctx) {
|
|
|
104
128
|
if (release.version === VERSION) {
|
|
105
129
|
// Nothing downloaded. The common case is an install that is already current, and paying
|
|
106
130
|
// for the whole package to be told so is a cost with nothing on the other side of it.
|
|
131
|
+
installed = VERSION;
|
|
132
|
+
outcome = "unchanged";
|
|
107
133
|
lines.push(` ${ctx.ui.success(ctx.t.update.already(release.version))}`);
|
|
108
134
|
}
|
|
109
135
|
else if (ctx.flags.dryRun) {
|
|
110
|
-
|
|
136
|
+
outcome = "dry-run";
|
|
137
|
+
lines.push(` ${ctx.t.update.wouldReplace(humanText(ctx.paths.lib), humanText(release.version))}`);
|
|
111
138
|
}
|
|
112
139
|
else if (!ctx.flags.auto && !ctx.canPrompt) {
|
|
113
140
|
// JSON never asks, including on a terminal. Explicit --auto supplies approval through
|
|
114
141
|
// the same branch as human output; absent approval, report the existing no-install result.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
else if (ctx.flags.auto || (await confirm(ctx.t.update.question(release.version))) === "yes") {
|
|
118
|
-
const tarball = await downloadRelease(release, ctx.t);
|
|
119
|
-
lines.push(` ${ctx.ui.accent(ctx.t.update.found(release.version, tarball.byteLength))}`);
|
|
120
|
-
installRelease(ctx.paths, tarball, ctx.t);
|
|
121
|
-
// **What landed, not what was claimed.** The registry's `version` is metadata; a stub or
|
|
122
|
-
// a mistake serving one number and a different tarball made `update` report a version it
|
|
123
|
-
// had not installed — and, because the no-op check compares that metadata against this
|
|
124
|
-
// process's own constant, swap again on every run forever.
|
|
125
|
-
replaced = installedVersion(ctx.paths) ?? release.version;
|
|
126
|
-
lines.push(` ${ctx.ui.success(ctx.ui.bold(ctx.t.update.replaced(replaced)))}`);
|
|
142
|
+
outcome = "approval-required";
|
|
143
|
+
lines.push(...receiptLines(ctx.t.update.needsApproval, ctx.ui.width - 2).map(line => ` ${line}`));
|
|
127
144
|
}
|
|
128
145
|
else {
|
|
129
|
-
lines.push(` ${ctx.t.update.
|
|
146
|
+
lines.push(` ${ctx.t.update.wouldReplace(humanText(ctx.paths.lib), humanText(release.version))}`);
|
|
147
|
+
preview();
|
|
148
|
+
if (ctx.flags.auto || (await confirm(ctx.t.update.question(release.version))) === "yes") {
|
|
149
|
+
const tarball = await downloadRelease(release, ctx.t);
|
|
150
|
+
lines.push(` ${ctx.ui.accent(ctx.t.update.found(release.version, tarball.byteLength))}`);
|
|
151
|
+
installRelease(ctx.paths, tarball, ctx.t);
|
|
152
|
+
installed = installedVersion(ctx.paths);
|
|
153
|
+
if (installed === null)
|
|
154
|
+
throw new ConfigError(ctx.t.update.versionUnknown);
|
|
155
|
+
replaced = installed;
|
|
156
|
+
outcome = "updated";
|
|
157
|
+
lines.push(` ${ctx.ui.success(ctx.ui.bold(ctx.t.update.replaced(installed)))}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
outcome = "declined";
|
|
161
|
+
lines.push(` ${ctx.t.update.declined}`);
|
|
162
|
+
}
|
|
130
163
|
}
|
|
131
164
|
}
|
|
132
165
|
const warnings = [];
|
|
@@ -134,20 +167,33 @@ export async function runUpdate(ctx) {
|
|
|
134
167
|
// `seedFiles` are constants in the copy that has just been replaced, so a release that moved
|
|
135
168
|
// the anchors would be reported against the old ones — which is the exact gap this command
|
|
136
169
|
// exists to close. Say the one thing that is still true and stop measuring.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
const inspectable = replaced === null ? stale ?? [] : [];
|
|
171
|
+
const routine = inspectable.filter(({ state }) => state === "missing" || state === "stale");
|
|
172
|
+
const problems = inspectable.filter(({ state }) => state !== "missing" && state !== "stale");
|
|
173
|
+
const fresh = stateIssue === "missing" && ![ctx.paths.agents, ctx.paths.tone, ctx.paths.presets, ctx.paths.skill].some(path => {
|
|
174
|
+
try {
|
|
175
|
+
return lstatSync(path, { throwIfNoEntry: false }) !== undefined;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (stateIssue !== null && !fresh)
|
|
182
|
+
warnings.push(ctx.t.update.stateIssue[stateIssue]);
|
|
183
|
+
if (routine.length)
|
|
184
|
+
lines.push("", ` ${ctx.t.update.refreshFiles(routine.length)}`);
|
|
185
|
+
if (problems.length) {
|
|
186
|
+
lines.push("", ` ${ctx.t.wiring.stateRoot}`, ` ${humanText(ctx.paths.root)}`);
|
|
187
|
+
for (const reason of ["modified", "foreign", "unreadable"]) {
|
|
188
|
+
const group = problems.filter(({ state }) => state === reason);
|
|
189
|
+
if (group.length)
|
|
190
|
+
warnings.push([ctx.t.doctor.seedReason[reason], ...group.map(({ seed }) => seed.label)].join("\n"));
|
|
149
191
|
}
|
|
150
192
|
}
|
|
193
|
+
const stateRecoveryRequired = !fresh && (stateIssue !== null || config?.seedVersion === 0 || inspectable.length > 0 || replaced !== null);
|
|
194
|
+
const recoveryCommand = fresh ? "tonedial init" : problems.length ? "tonedial doctor" : "tonedial doctor --fix";
|
|
195
|
+
if (fresh || stateRecoveryRequired)
|
|
196
|
+
lines.push("", ` ${fresh ? ctx.t.update.setupNeeded : problems.length ? ctx.t.update.reviewState : ctx.t.update.thenRefresh}`, ` ${ctx.ui.accent(recoveryCommand)}`);
|
|
151
197
|
return {
|
|
152
198
|
lines,
|
|
153
199
|
json: {
|
|
@@ -155,17 +201,18 @@ export async function runUpdate(ctx) {
|
|
|
155
201
|
shape,
|
|
156
202
|
command,
|
|
157
203
|
replaced,
|
|
204
|
+
installedVersion: installed,
|
|
205
|
+
outcome,
|
|
158
206
|
seedVersion: config?.seedVersion ?? null,
|
|
159
207
|
stateIssue,
|
|
160
|
-
stateRecoveryRequired
|
|
161
|
-
recoveryCommand
|
|
208
|
+
stateRecoveryRequired,
|
|
209
|
+
recoveryCommand,
|
|
162
210
|
// Null after a swap: this process is the old package, and reporting its constants as the
|
|
163
211
|
// installed ones would be a claim it cannot make.
|
|
164
212
|
packageSeedVersion: replaced === null ? SEED_VERSION : null,
|
|
165
213
|
staleSeeds: replaced === null ? (stale?.map(({ seed }) => seed.label) ?? null) : null,
|
|
166
214
|
seedIssues: replaced === null ? (stale?.map(({ seed, state }) => ({ path: seed.path, reason: state })) ?? null) : null,
|
|
167
|
-
//
|
|
168
|
-
// must not read silence here as "you are up to date".
|
|
215
|
+
// npm owns registry/cache resolution; only the curl branch observes remote metadata.
|
|
169
216
|
checkedRemote: shape === "curl",
|
|
170
217
|
},
|
|
171
218
|
warnings,
|
package/dist/commands/use.js
CHANGED
|
@@ -65,7 +65,7 @@ export async function runUse(ctx, args) {
|
|
|
65
65
|
risk: ctx.t.notice.risk,
|
|
66
66
|
roleplay: config.axes.playfulness >= 4 ? ctx.t.notice.roleplay : null,
|
|
67
67
|
};
|
|
68
|
-
const notices = [notice.safeword, ...(notice.riskAxes ? [notice.riskAxes] : [])
|
|
68
|
+
const notices = [notice.safeword, ...(notice.riskAxes ? [notice.riskAxes] : [])];
|
|
69
69
|
const lines = [
|
|
70
70
|
...wrap(ctx.t.preset.active(truncate(humanText(preset.displayName), ui.width - 12)), ui.width - 2)
|
|
71
71
|
.map((line) => ` ${ui.success(ui.bold(line))}`),
|
|
@@ -74,7 +74,7 @@ export async function runUse(ctx, args) {
|
|
|
74
74
|
// Folded here rather than at the printer: `lines` are laid out by the command that built
|
|
75
75
|
// them, and a printer that reflowed them would break every column in the gauge block.
|
|
76
76
|
"",
|
|
77
|
-
...notices.flatMap((n) => wrap(humanText(n), ui.width - 2).map((folded) => ` ${n === notice.riskAxes ? ui.risk(folded) :
|
|
77
|
+
...notices.flatMap((n) => wrap(humanText(n), ui.width - 2).map((folded) => ` ${n === notice.riskAxes ? ui.risk(folded) : folded}`)),
|
|
78
78
|
...project,
|
|
79
79
|
...nextStep("tonedial show", ui),
|
|
80
80
|
];
|
|
@@ -93,8 +93,7 @@ export async function runUse(ctx, args) {
|
|
|
93
93
|
project: projectPath,
|
|
94
94
|
},
|
|
95
95
|
warnings: [
|
|
96
|
-
...(
|
|
97
|
-
...checkCombos(config.axes, config.persona, config.speech_level).map((w) => combination(w, ctx.t)),
|
|
96
|
+
...checkCombos(config.axes).map((w) => combination(w, ctx.t)),
|
|
98
97
|
],
|
|
99
98
|
};
|
|
100
99
|
}
|
package/dist/commands/wire.js
CHANGED
|
@@ -453,7 +453,7 @@ function summary(ctx, reason, tools, action = "connect") {
|
|
|
453
453
|
return receiptLines(action === "disconnect" ? ctx.t.wiring.alreadyDisconnected(named) : ctx.t.wiring.alreadyMatch(named), ctx.ui.width - 2)
|
|
454
454
|
.map((line) => ` ${ctx.ui.bold(line)}`);
|
|
455
455
|
if (reason === "dry-run")
|
|
456
|
-
return [` ${ctx.ui.
|
|
456
|
+
return [` ${ctx.ui.dim(ctx.t.wiring.dryRun)}`];
|
|
457
457
|
if (reason === "declined")
|
|
458
458
|
return [` ${ctx.t.wiring.declined}`];
|
|
459
459
|
return [
|
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/combos.js
CHANGED
|
@@ -30,45 +30,13 @@ const COMPOSITES = [
|
|
|
30
30
|
export function composites(axes) {
|
|
31
31
|
return COMPOSITES.filter((c) => c.fires(axes)).map((c) => c.line);
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const w = [];
|
|
39
|
-
const add = (layer, rule, id) => w.push({ layer, rule, id });
|
|
40
|
-
if (axes.refusal >= 4 && axes.compliance >= 4) {
|
|
41
|
-
add("forbidden", "refusal≥4 × compliance≥4", "refusalCompliance");
|
|
42
|
-
}
|
|
43
|
-
if (persona) {
|
|
44
|
-
const selfRef = persona.address?.self_ref ?? null;
|
|
45
|
-
const userRef = persona.address?.user_ref ?? null;
|
|
46
|
-
if (axes.formality === 5 && selfRef && INFORMAL_SELF.test(selfRef)) {
|
|
47
|
-
add("awkward", "formality 5 × informal self_ref", "formalSelfRef");
|
|
48
|
-
}
|
|
49
|
-
if (axes.formality <= 2 && speechLevel === "plain" && userRef && DEFERENTIAL_USER.test(userRef)) {
|
|
50
|
-
add("awkward", "formality≤2 × deferential user_ref", "casualUserRef");
|
|
51
|
-
}
|
|
52
|
-
if (persona.age === "teens" && axes.formality === 5) {
|
|
53
|
-
add("awkward", "age: teens × formality 5", "teensFormal");
|
|
54
|
-
}
|
|
55
|
-
if (persona.age === "50s+" && selfRef && INFORMAL_SELF.test(selfRef)) {
|
|
56
|
-
add("awkward", "age: 50s+ with a junior self_ref", "seniorJuniorSelfRef");
|
|
57
|
-
}
|
|
58
|
-
// The spec's eighth awkward rule — `age` contradicting the relative age implied by
|
|
59
|
-
// `address` — is **not implemented**. It needs a kinship lexicon that maps 형 or 누나 to a
|
|
60
|
-
// relative age, in a design that takes address terms as free-form text in any language.
|
|
61
|
-
// Recorded rather than dropped: the rule is right and the input it needs does not exist.
|
|
62
|
-
// Measured, not predicted: across five `terse-operator` runs the configured address never
|
|
63
|
-
// surfaced once, because a maximally terse reply has no natural slot for a vocative.
|
|
64
|
-
// The user is told rather than obeyed, since the alternative is forcing unnatural output.
|
|
65
|
-
if ((selfRef || userRef) && (axes.warmth <= 2 || axes.verbosity <= 2)) {
|
|
66
|
-
add("ineffective", "address set while warmth≤2 or verbosity≤2", "addressUnlikely");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return w;
|
|
33
|
+
/** Independent persona choices are valid; warn only when axis instructions conflict. */
|
|
34
|
+
export function check(axes) {
|
|
35
|
+
return axes.refusal >= 4 && axes.compliance >= 4
|
|
36
|
+
? [{ layer: "forbidden", rule: "refusal≥4 × compliance≥4", id: "refusalCompliance" }]
|
|
37
|
+
: [];
|
|
70
38
|
}
|
|
71
|
-
/** The axes a preset moves off their default
|
|
39
|
+
/** The axes a preset moves off their default; custom profiles have no count restriction. */
|
|
72
40
|
export function nonDefaultAxes(axes, defaults) {
|
|
73
41
|
// Walks AXIS_ORDER rather than Object.keys: map iteration order follows the JSON file, and a
|
|
74
42
|
// hand-edited config carrying a stray key would otherwise be reported as a non-default axis.
|
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,8 +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"];
|
|
14
|
-
export const NOTE_MAX = 200;
|
|
13
|
+
export const SPEECH_LEVELS = ["auto", "polite", "plain", "rough"];
|
|
15
14
|
export const WIRING_SCOPES = ["global", "project"];
|
|
16
15
|
export const RESOURCE_KINDS = ["hook", "skill", "shell", "reader", "command"];
|
|
17
16
|
export const RESOURCE_TYPES = ["file", "directory", "symlink"];
|
|
@@ -854,20 +853,32 @@ export const isAxis = (name) => AXIS_ORDER.includes(name);
|
|
|
854
853
|
/**
|
|
855
854
|
* Everything `set <key>=<value>` takes that is not an axis.
|
|
856
855
|
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
* 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.
|
|
860
858
|
*
|
|
861
|
-
*
|
|
862
|
-
* test that drives every entry through `set`. A branch added to `assign` and never listed here
|
|
863
|
-
* is invisible: it would not appear in `--explain` and `nearest` would not suggest it, and
|
|
864
|
-
* 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.
|
|
865
860
|
*/
|
|
866
861
|
export const SETTING_KEYS = [
|
|
867
862
|
"safeword", "speech_level", "language",
|
|
868
863
|
"persona.age", "persona.gender", "persona.self_ref", "persona.user_ref", "persona.note",
|
|
869
864
|
"persona.user", "persona.agent",
|
|
870
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
|
+
}
|
|
871
882
|
/** Levenshtein-lite: enough to say "did you mean critique?" and no more. */
|
|
872
883
|
export function nearest(name, candidates) {
|
|
873
884
|
const distance = (a, b) => {
|