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.
@@ -1,12 +1,14 @@
1
1
  // Bounded release download and extraction; the candidate installer owns package replacement.
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
- import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { accessSync, constants, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
- import { join } from "node:path";
6
+ import { dirname, isAbsolute, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
7
8
  import { gunzipSync } from "node:zlib";
8
9
  import { ConfigError } from "./config.js";
9
10
  import { decodeUtf8, getBounded, urlIdentity } from "./net.js";
11
+ import { fileIdentity, onPath } from "./paths.js";
10
12
  /** Where releases come from. Overridable for tests, exactly as `install.sh` overrides it. */
11
13
  export const REGISTRY = "https://registry.npmjs.org";
12
14
  /**
@@ -23,12 +25,76 @@ const METADATA_BOUNDS = { maxBytes: 1024 * 1024, timeoutMs: RELEASE_TIMEOUT_MS }
23
25
  export function installedVersion(paths) {
24
26
  try {
25
27
  const raw = JSON.parse(readFileSync(join(paths.lib, "package.json"), "utf8"));
26
- return typeof raw.version === "string" ? raw.version : null;
28
+ return raw.name === "tonedial" && typeof raw.version === "string" && /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]+)?$/.test(raw.version)
29
+ ? raw.version : null;
27
30
  }
28
31
  catch {
29
32
  return null;
30
33
  }
31
34
  }
35
+ /** Bind npm's global target to this running copy before allowing package-manager writes. */
36
+ export function inspectNpmInstall(env, t) {
37
+ const executable = onPath("npm", env);
38
+ if (executable === null)
39
+ throw new ConfigError(t.update.npmMissing);
40
+ const entry = resolve(executable);
41
+ const query = (args) => {
42
+ const result = spawnSync(entry, args, { env, encoding: "utf8", timeout: RELEASE_TIMEOUT_MS, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
43
+ const value = result.stdout?.trim();
44
+ if (result.status !== 0 || !value || !isAbsolute(value) || /[\r\n\0]/.test(value))
45
+ throw new ConfigError(t.update.npmScopeUnknown);
46
+ return resolve(value);
47
+ };
48
+ const prefix = query(["prefix", "-g"]);
49
+ const root = join(query(["root", "-g", "--prefix", prefix]), "tonedial");
50
+ let install;
51
+ try {
52
+ const own = fileURLToPath(new URL("../../", import.meta.url));
53
+ const current = fileIdentity(own);
54
+ const target = fileIdentity(root);
55
+ const version = installedVersion({ lib: root });
56
+ if (lstatSync(root, { throwIfNoEntry: false })?.isSymbolicLink() || current === null || target === null || current.dev !== target.dev || current.ino !== target.ino || version === null) {
57
+ throw new ConfigError(t.update.npmScopeMismatch);
58
+ }
59
+ install = { entry, prefix, root, version };
60
+ }
61
+ catch {
62
+ throw new ConfigError(t.update.npmScopeMismatch);
63
+ }
64
+ try {
65
+ // npm replaces the package by renaming it in this parent, not by editing its files.
66
+ accessSync(dirname(root), constants.W_OK | constants.X_OK);
67
+ }
68
+ catch (cause) {
69
+ const code = cause.code;
70
+ if (code === "EACCES" || code === "EPERM")
71
+ throw new ConfigError(t.update.npmPermissionDenied(code, prefix));
72
+ throw new ConfigError(t.update.npmScopeUnknown);
73
+ }
74
+ return install;
75
+ }
76
+ /** npm owns replacement; captured output cannot corrupt JSON or disclose npm configuration. */
77
+ export function updateNpmInstall(install, env, t) {
78
+ const current = inspectNpmInstall(env, t);
79
+ if (current.entry !== install.entry || current.prefix !== install.prefix || current.root !== install.root || current.version !== install.version)
80
+ throw new ConfigError(t.update.npmScopeMismatch);
81
+ const result = spawnSync(install.entry, ["update", "-g", "tonedial", "--prefix", install.prefix, "--json"], { env, encoding: "utf8", timeout: 120_000, maxBuffer: RELEASE_MAX_BYTES, stdio: ["ignore", "pipe", "pipe"] });
82
+ if (result.status !== 0) {
83
+ let code;
84
+ try {
85
+ code = JSON.parse(result.stdout ?? "null")?.error?.code;
86
+ }
87
+ catch { /* npm may fail before JSON output. */ }
88
+ code ??= /^npm (?:error|ERR!) code (EACCES|EPERM)\s*$/m.exec(result.stderr ?? "")?.[1];
89
+ if (code === "EACCES" || code === "EPERM")
90
+ throw new ConfigError(t.update.npmPermissionDenied(code, install.prefix));
91
+ throw new ConfigError(t.update.npmFailed);
92
+ }
93
+ const version = installedVersion({ lib: install.root });
94
+ if (version === null)
95
+ throw new ConfigError(t.update.versionUnknown);
96
+ return version;
97
+ }
32
98
  /** Whether the one external tool this needs is here. Checked before anything is downloaded. */
33
99
  export function haveTar() {
34
100
  return spawnSync("tar", ["--version"], { stdio: "ignore" }).status === 0;
@@ -29,7 +29,14 @@ or repair decision. Never edit or recreate them directly.
29
29
  /** Both entry points distinguish current tone settings from saved profiles. */
30
30
  const PRESETS_MD = `## Current settings and saved presets
31
31
 
32
- \`tonedial show --json\` reports the current settings. \`show <slug> --json\` previews that
32
+ \`tonedial get --json\` reports the current editable settings in a flat \`values\` object.
33
+ \`get <key> --json\` selects one of those keys, preserving numbers, literal text and nulls.
34
+ The keys are the twelve axes, \`language\`, \`speech_level\`, \`safeword\`, and
35
+ \`persona.age\`, \`persona.gender\`, \`persona.self_ref\`, \`persona.user_ref\`,
36
+ \`persona.note\`, \`persona.user\`, and \`persona.agent\`.
37
+ \`language\` selects the CLI interface, not the agent's reply language.
38
+ Read-only \`get\` neither changes settings nor reloads this conversation.
39
+ \`tonedial show --json\` adds the overview and artifact metadata. \`show <slug> --json\` previews that
33
40
  preset's saved tone settings. \`unsaved\` on the current result means the current tone settings
34
41
  have not been saved back to a profile; the current configuration itself is persisted.
35
42
  For an explicit request to save a profile, use \`tonedial save "<display name>" --json\`.
@@ -86,7 +93,7 @@ Persona, address, safeword, and \`speech_level\` are separate from the axes.
86
93
  \`playfulness\` increases the frequency and exaggeration of the chosen role's expressions:
87
94
  literal wording at 1, one restrained touch at 2, two recognizable touches at 3,
88
95
  frequent theatrical idioms at 4, and a character voice throughout at 5.
89
- Role slang and figurative language are not filtered; the CLI warns about rude or violent idioms.
96
+ Role slang and figurative language are not filtered.
90
97
  \`profanity\` still grades actual swearing; \`bravado\` grades claims about effort.
91
98
  Change one only when the user explicitly asks for that setting; preserve every other setting.
92
99
  Use the user's chosen text and values, not the illustrative values below.
@@ -107,6 +114,7 @@ interpolating it into shell code.
107
114
  | Persona manner or role note | \`tonedial set "persona.note=Patient guide" --json\` |
108
115
  | Safeword phrase | \`tonedial set "safeword=plain answer please" --json\` |
109
116
  | Honorific register system | \`tonedial set speech_level=polite --json\` |
117
+ | Rough, cheeky non-honorific endings | \`tonedial set speech_level=rough --json\` |
110
118
 
111
119
  Age bands are \`unspecified\`, \`teens\`, \`20s\`, \`30s\`, \`40s\`, and \`50s+\`.
112
120
  Gender values are \`unspecified\`, \`masculine\`, \`feminine\`, and \`neutral\`.
@@ -121,15 +129,17 @@ Agent experience and specialty are roleplay, not verified credentials or tool ca
121
129
  Both descriptions lose to axes and task/permission rules, and may influence content.
122
130
  A persona note expresses manner or role, loses to axes, and may also influence
123
131
  content; it is not a way to override task or permission rules.
124
- The CLI warns above 200 Unicode code points instead of rejecting the note; report its warnings.
132
+ Persona notes have no length limit.
125
133
 
126
134
  ToneDial has no guard toggle or automatic emergency register. All axis levels 1–5 are choices
127
135
  the user can request. Report advisory \`riskAxes\` and combination notices without lowering
128
136
  values, blocking the change, or asking for additional tone-risk confirmation. The host's task
129
137
  and permission rules still apply independently.
130
- For \`speech_level\`, choose \`polite\`, \`plain\`, or \`auto\` only when the user requests that
138
+ For \`speech_level\`, choose \`auto\`, \`polite\`, \`plain\`, or \`rough\` only when the user requests that
131
139
  register system. \`auto\` chooses address forms for the current relationship and scene,
132
140
  keeping them consistent within each reply; \`polite\` and \`plain\` fix those forms.
141
+ \`plain\` means ordinary non-honorific forms; \`rough\` requests rough, cheeky non-honorific
142
+ sentence endings. Preserve the configured address terms and every independent axis.
133
143
  It has no effect where the language lacks an honorific distinction. A request for more or
134
144
  less formality changes wording and distance within the chosen forms, without silently changing
135
145
  \`speech_level\`. Familiar polite, formal plain and affectionate swearing are valid combinations;
@@ -198,12 +208,13 @@ the user to complete CLI installation and explicit setup; do not install it or g
198
208
  ## Read the current state first
199
209
 
200
210
  \`\`\`bash
201
- tonedial show --json
211
+ tonedial get --json
202
212
  \`\`\`
203
213
 
204
- The payload carries \`axes\` (twelve axes, each 1–5), \`defaults\`, \`groups\`, \`kinds\`,
205
- \`riskAxes\`, \`persona\`, \`safeword\`, \`speech_level\`, and \`chars\`. Every command takes \`--json\` and answers with a
206
- \`schema\` field.
214
+ The \`values\` object carries the twelve axis levels and the separate settings under their
215
+ editable keys. An unset setting is \`null\`; description text is returned in full.
216
+ For the formatted overview, preset state and advisory \`riskAxes\`, use \`tonedial show --json\`.
217
+ Every command takes \`--json\` and answers with a \`schema\` field.
207
218
  ${JSON_MD}
208
219
  Otherwise report the failed step
209
220
  and stop that flow. Never report a failed mutation as saved; failures after a successful mutation
@@ -230,7 +241,7 @@ Report the movement back to the user in their own terms, from the \`moves\` fiel
230
241
  - "stop flattering me" → \`flattery=1\`. Not \`warmth\`, which is a different observable.
231
242
  - "be brief" → \`verbosity\` down. Change \`care\` only for requested coverage or organization changes.
232
243
  - "more casual" → \`formality\` down. Change the register system only on an explicit request,
233
- such as "use plain forms" → \`speech_level=plain\`.
244
+ such as "use plain forms" → \`speech_level=plain\`, or "use rough, cheeky plain forms" → \`speech_level=rough\`.
234
245
 
235
246
  For a named preset, run \`tonedial list --json\` then \`tonedial use <slug> --json\`.
236
247
  An explicit agent character or role request uses \`persona.agent\`; an explicit request to
@@ -308,7 +319,7 @@ During a test conversation, do not mutate ToneDial settings or perform work thro
308
319
  */
309
320
  export const SKILL_TRAITS_MD = `# Axes
310
321
 
311
- Twelve axes, each 1 to 5. Read the live values with \`tonedial show --json\`; this file says what
322
+ Twelve axes, each 1 to 5. Read the live values with \`tonedial get --json\`; this file says what
312
323
  each one **owns**, which is what tells you which axis a request is actually about.
313
324
 
314
325
  Axes name distinct aspects of a reply. Map the aspect the user named rather than changing
@@ -369,7 +380,6 @@ depends on the host and model.
369
380
  - Functional status emoji can accompany low \`warmth\`; they are structure, not affection.
370
381
  - Required task formats take precedence: a code or JSON result must not become decorative prose.
371
382
  - \`refusal\` ≥ 4 with \`compliance\` ≥ 4 contradicts itself. The CLI warns and does not block.
372
- - \`formality\` ≥ 4 with \`profanity\` ≥ 4 holds but reads oddly.
373
383
  - \`warmth\` ≥ 4 with \`profanity\` ≥ 4, and \`compliance\` ≤ 2 with \`conviction\` ≤ 2, each get a
374
384
  composite sentence in the artifact rather than being left to collide.
375
385
  - Care 5 adds a caring organizer signature; high playfulness with high bravado adds a cocky expert signature. These are static instructions selected by the combination layer.
@@ -381,9 +391,11 @@ do not prove actual host activation or model compliance.
381
391
 
382
392
  ## Settings that are not axes
383
393
 
384
- \`speech_level\` picks address forms — \`auto\`, \`polite\`, or \`plain\` — where the language
394
+ \`speech_level\` picks address forms — \`auto\`, \`polite\`, \`plain\`, or \`rough\` — where the language
385
395
  has an honorific distinction, and does nothing where it does not. Auto adapts to the current
386
- relationship and scene, staying consistent within a reply. \`formality\` sets distance inside
396
+ relationship and scene, staying consistent within a reply. Plain uses ordinary non-honorific forms;
397
+ rough uses rough, cheeky non-honorific endings while keeping configured address terms.
398
+ \`formality\` sets distance inside
387
399
  the chosen forms; warmth and profanity keep their selected effects. The persona layer (\`age\`, \`gender\`, \`self_ref\`, \`user_ref\`,
388
400
  \`user\`, \`agent\`, \`note\`) and the safeword are included in bundled and saved custom profiles.
389
401
  \`persona.user\` is user context and \`persona.agent\` a fictional agent role, each limited to
@@ -393,7 +405,7 @@ them, and \`export\`/\`import\` transfer them. Older axis-only presets keep the
393
405
  The seven bundled profiles supply localized role and address defaults; custom text remains literal.
394
406
  Playfulness grades role idiom frequency from literal wording at 1 to theatrical voice throughout at 5.
395
407
  Role slang and metaphors are not filtered or suppressed by low profanity or bravado.
396
- Profanity grades actual swearing; bravado grades effort claims. The CLI carries advisory examples.
408
+ Profanity grades actual swearing; bravado grades effort claims.
397
409
  `;
398
410
  /**
399
411
  * The Claude Code `/tone` command, installed only by `init --commands`.
@@ -403,12 +415,12 @@ Profanity grades actual swearing; bravado grades effort claims. The CLI carries
403
415
  * command name stays `/tone` even though the tool is `tonedial`: it is what a person types
404
416
  * mid-sentence, and the two extra characters are paid every time.
405
417
  */
406
- export const TONE_COMMAND_VERSION = 8;
418
+ export const TONE_COMMAND_VERSION = 11;
407
419
  export const TONE_COMMAND_MD = `---
408
420
  description: Check, change, or reload this agent's tone, preset, or ToneDial settings
409
421
  ---
410
422
 
411
- Run \`tonedial show --json\` and report the requested current settings in plain language.
423
+ Run \`tonedial get --json\` and report the requested current settings from \`values\` in plain language.
412
424
  Use the same installed CLI and environment throughout.
413
425
  ${JSON_MD}
414
426
  Otherwise report the failed step and stop that flow.
package/dist/ui/format.js CHANGED
@@ -11,7 +11,7 @@ export function nextStep(command, ui) {
11
11
  /** One advisory notice from the shared axis metadata, without changing selected levels. */
12
12
  export function riskWarnings(axes, t) {
13
13
  const risks = riskyAxes(axes);
14
- return [...(risks.length ? [t.notice.riskAxes(risks)] : []), ...(axes.playfulness >= 4 ? [t.notice.roleplay] : [])];
14
+ return risks.length ? [t.notice.riskAxes(risks)] : [];
15
15
  }
16
16
  /** Two retained phases restore their shared config in reverse order, with copyable IDs. */
17
17
  export function recoveryRollbackLines(t, appId, nativeId) {
@@ -64,10 +64,7 @@ export function num(n) {
64
64
  export function artifactSize(size, t) {
65
65
  return t.artifact.size(num(size.chars));
66
66
  }
67
- /** Render combination warnings with the same layer distinction in every command. */
67
+ /** Contradictions stay advisory and never block the selected values. */
68
68
  export function combination(w, t) {
69
- const sentence = t.combos[w.id];
70
- return w.layer === "forbidden"
71
- ? `${t.combos.contradiction(w.rule)} ${sentence} ${t.combos.nothingBlocked}`
72
- : `${t.combos.reads(w.rule)} ${sentence}`;
69
+ return `${t.combos.contradiction(w.rule)} ${t.combos[w.id]} ${t.combos.nothingBlocked}`;
73
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonedial",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Set a coding agent's manner once and keep it across every project.",
5
5
  "license": "MIT",
6
6
  "type": "module",