tonedial 0.1.0 → 0.1.2

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.
@@ -3,10 +3,12 @@ import { spawnSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
4
  import { 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 { 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,55 @@ 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
+ try {
51
+ const own = fileURLToPath(new URL("../../", import.meta.url));
52
+ const current = fileIdentity(own);
53
+ const target = fileIdentity(root);
54
+ const version = installedVersion({ lib: root });
55
+ if (lstatSync(root, { throwIfNoEntry: false })?.isSymbolicLink() || current === null || target === null || current.dev !== target.dev || current.ino !== target.ino || version === null) {
56
+ throw new ConfigError(t.update.npmScopeMismatch);
57
+ }
58
+ return { entry, prefix, root, version };
59
+ }
60
+ catch {
61
+ throw new ConfigError(t.update.npmScopeMismatch);
62
+ }
63
+ }
64
+ /** npm owns replacement; captured output cannot corrupt JSON or disclose npm configuration. */
65
+ export function updateNpmInstall(install, env, t) {
66
+ const current = inspectNpmInstall(env, t);
67
+ if (current.entry !== install.entry || current.prefix !== install.prefix || current.root !== install.root || current.version !== install.version)
68
+ throw new ConfigError(t.update.npmScopeMismatch);
69
+ 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"] });
70
+ if (result.status !== 0)
71
+ throw new ConfigError(t.update.npmFailed);
72
+ const version = installedVersion({ lib: install.root });
73
+ if (version === null)
74
+ throw new ConfigError(t.update.versionUnknown);
75
+ return version;
76
+ }
32
77
  /** Whether the one external tool this needs is here. Checked before anything is downloaded. */
33
78
  export function haveTar() {
34
79
  return spawnSync("tar", ["--version"], { stdio: "ignore" }).status === 0;
@@ -5,6 +5,7 @@ export const SKILL_NAME = "tonedial";
5
5
  /** The current command envelope, shared by both installed control entry points. */
6
6
  const JSON_MD = `Require zero exit status and a JSON result with \`ok: true\` and \`schema: 3\`
7
7
  for every successful CLI step before continuing. Parse JSON rather than scraping human output.
8
+ \`chars\` reports the current TONE.md Unicode character count; no size target or maximum applies.
8
9
  Schema 3 removes guard and ceiling fields and reports advisory \`riskAxes\`; persisted config
9
10
  remains schema 2. Custom profiles use preset schema 2; legacy schema 1 and journal formats
10
11
  remain supported. Never send \`guards\` or
@@ -85,7 +86,7 @@ Persona, address, safeword, and \`speech_level\` are separate from the axes.
85
86
  \`playfulness\` increases the frequency and exaggeration of the chosen role's expressions:
86
87
  literal wording at 1, one restrained touch at 2, two recognizable touches at 3,
87
88
  frequent theatrical idioms at 4, and a character voice throughout at 5.
88
- Role slang and figurative language are not filtered; the CLI warns about rude or violent idioms.
89
+ Role slang and figurative language are not filtered.
89
90
  \`profanity\` still grades actual swearing; \`bravado\` grades claims about effort.
90
91
  Change one only when the user explicitly asks for that setting; preserve every other setting.
91
92
  Use the user's chosen text and values, not the illustrative values below.
@@ -98,6 +99,7 @@ interpolating it into shell code.
98
99
  | What to call the user only | \`tonedial set "persona.user_ref=friend" --json\` |
99
100
  | What to call yourself only | \`tonedial set "persona.self_ref=guide" --json\` |
100
101
  | Both address terms, self first and user second | \`tonedial address "assistant" "reader" --json\` |
102
+ | Both role descriptions, agent first and user second | \`tonedial persona "Fictional database reviewer" "Developer" --json\` |
101
103
  | Persona age band | \`tonedial set persona.age=30s --json\` |
102
104
  | Persona gender | \`tonedial set persona.gender=neutral --json\` |
103
105
  | User background and preferences | \`tonedial set "persona.user=Developer who likes concise technical answers" --json\` |
@@ -113,11 +115,13 @@ An empty value clears only the named persona field or safeword, for example
113
115
  \`persona.user\` describes the user; \`persona.agent\` describes the agent's fictional role.
114
116
  Each accepts at most 200 Unicode code points; never silently truncate a longer request.
115
117
  Preserve the other fields and ask the user to shorten text rejected by the CLI.
118
+ In a terminal, bare \`tonedial persona\` asks for both descriptions; Enter keeps a value and \`""\` clears it.
119
+ \`show\` displays both descriptions and their character counts; a slug selects the saved profile.
116
120
  Agent experience and specialty are roleplay, not verified credentials or tool capabilities.
117
121
  Both descriptions lose to axes and task/permission rules, and may influence content.
118
122
  A persona note expresses manner or role, loses to axes, and may also influence
119
123
  content; it is not a way to override task or permission rules.
120
- The CLI warns above 200 Unicode code points instead of rejecting the note; report its warnings.
124
+ Persona notes have no length limit.
121
125
 
122
126
  ToneDial has no guard toggle or automatic emergency register. All axis levels 1–5 are choices
123
127
  the user can request. Report advisory \`riskAxes\` and combination notices without lowering
@@ -242,7 +246,7 @@ ${SETUP_MD}
242
246
 
243
247
  ## Apply it in this session
244
248
 
245
- Successful \`set\`, \`address\`, \`use\`, \`reset\`, \`save\`, and approved \`create\` commands recompile \`TONE.md\`; that is a saved state
249
+ Successful \`set\`, \`address\`, \`persona\`, \`use\`, \`reset\`, \`save\`, and approved \`create\` commands recompile \`TONE.md\`; that is a saved state
246
250
  change, not evidence of a session reload.
247
251
 
248
252
  ${RELOAD_MD}
@@ -365,7 +369,6 @@ depends on the host and model.
365
369
  - Functional status emoji can accompany low \`warmth\`; they are structure, not affection.
366
370
  - Required task formats take precedence: a code or JSON result must not become decorative prose.
367
371
  - \`refusal\` ≥ 4 with \`compliance\` ≥ 4 contradicts itself. The CLI warns and does not block.
368
- - \`formality\` ≥ 4 with \`profanity\` ≥ 4 holds but reads oddly.
369
372
  - \`warmth\` ≥ 4 with \`profanity\` ≥ 4, and \`compliance\` ≤ 2 with \`conviction\` ≤ 2, each get a
370
373
  composite sentence in the artifact rather than being left to collide.
371
374
  - 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.
@@ -389,7 +392,7 @@ them, and \`export\`/\`import\` transfer them. Older axis-only presets keep the
389
392
  The seven bundled profiles supply localized role and address defaults; custom text remains literal.
390
393
  Playfulness grades role idiom frequency from literal wording at 1 to theatrical voice throughout at 5.
391
394
  Role slang and metaphors are not filtered or suppressed by low profanity or bravado.
392
- Profanity grades actual swearing; bravado grades effort claims. The CLI carries advisory examples.
395
+ Profanity grades actual swearing; bravado grades effort claims.
393
396
  `;
394
397
  /**
395
398
  * The Claude Code `/tone` command, installed only by `init --commands`.
@@ -399,7 +402,7 @@ Profanity grades actual swearing; bravado grades effort claims. The CLI carries
399
402
  * command name stays `/tone` even though the tool is `tonedial`: it is what a person types
400
403
  * mid-sentence, and the two extra characters are paid every time.
401
404
  */
402
- export const TONE_COMMAND_VERSION = 7;
405
+ export const TONE_COMMAND_VERSION = 9;
403
406
  export const TONE_COMMAND_MD = `---
404
407
  description: Check, change, or reload this agent's tone, preset, or ToneDial settings
405
408
  ---
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) {
@@ -61,13 +61,10 @@ export function num(n) {
61
61
  return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
62
62
  }
63
63
  /** The one shape the artifact's size is reported in, by every command that recompiles. */
64
- export function budget(size, t) {
65
- return t.artifact.budget(num(size.chars), num(size.cap));
64
+ export function artifactSize(size, t) {
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.0",
3
+ "version": "0.1.2",
4
4
  "description": "Set a coding agent's manner once and keep it across every project.",
5
5
  "license": "MIT",
6
6
  "type": "module",