@hraness/dawg 0.6.0 → 0.7.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.
Files changed (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Ctrl-k menu rows for the wind engine (0.6.1): the "Winds and brass" group
3
+ * under Sound › instruments, and the WIND_PARAMS rows under Sound ›
4
+ * Parameters on a wind-engine track. Every row runs a `wind …` command.
5
+ */
6
+ import type { EnumParam, NumberParam } from "../../core/params.ts";
7
+ import type { Track } from "../../core/score.ts";
8
+ import {
9
+ DEFAULT_WIND_PRESET,
10
+ WIND_GROUPS,
11
+ WIND_INSTRUMENT,
12
+ WIND_PARAMS,
13
+ WIND_PRESETS,
14
+ WIND_PRESET_NAMES,
15
+ WIND_SIMPLE,
16
+ windSettings,
17
+ type WindPresetName,
18
+ } from "../../core/winds.ts";
19
+ import { num, specStep, type MenuNode } from "./menu.ts";
20
+
21
+ function presetRow(name: WindPresetName): MenuNode {
22
+ return {
23
+ kind: "action",
24
+ label: `${name.padEnd(13)} ${WIND_PRESETS[name].doc}`,
25
+ command: `wind ${name}`,
26
+ help: WIND_PRESETS[name].styles,
27
+ };
28
+ }
29
+
30
+ /** Sound › instruments › winds and brass: flutes, reeds, brass. */
31
+ export function windsMenu(): MenuNode {
32
+ return {
33
+ kind: "menu",
34
+ id: "winds",
35
+ label: "winds and brass",
36
+ detail: `${WIND_PRESET_NAMES.length} blown · flute, sax, trumpet …`,
37
+ help: "breath-driven waveguides: flutes, reeds and brass; wind players 4 for a section",
38
+ build: () =>
39
+ WIND_GROUPS.map((group): MenuNode => ({
40
+ kind: "menu",
41
+ id: `winds:${group.label.toLowerCase()}`,
42
+ label: group.label,
43
+ detail: group.presets.slice(0, 3).join(", ") + " …",
44
+ help: `${group.label.toLowerCase()} on the wind engine`,
45
+ build: () => group.presets.map(presetRow),
46
+ })),
47
+ };
48
+ }
49
+
50
+ function numberRow(
51
+ name: string,
52
+ spec: NumberParam,
53
+ value: number,
54
+ fallback: number,
55
+ ): MenuNode {
56
+ return {
57
+ kind: "number",
58
+ label: name,
59
+ value,
60
+ min: spec.min,
61
+ max: spec.max,
62
+ step: specStep(spec),
63
+ format: (v) => (spec.unit ? `${num(v)} ${spec.unit}` : num(v)),
64
+ command: (v) => `wind ${name} ${num(v)}`,
65
+ reset: `wind ${name} off`,
66
+ help: `${spec.doc} · preset ${num(fallback)} · x resets`,
67
+ };
68
+ }
69
+
70
+ /** Sound › Parameters rows on a wind-engine track. */
71
+ export function windParameterNodes(track: Track): MenuNode[] {
72
+ if (track.instrument !== WIND_INSTRUMENT || !track.wind) return [];
73
+ const wind = track.wind;
74
+ const preset = WIND_PRESETS[wind.preset ?? DEFAULT_WIND_PRESET].settings;
75
+ const resolved = windSettings(wind) as Readonly<
76
+ Record<string, number | string | boolean>
77
+ >;
78
+ const fallback = preset as Readonly<
79
+ Record<string, number | string | boolean>
80
+ >;
81
+ const row = (name: string): MenuNode | undefined => {
82
+ const spec = WIND_PARAMS[name]!;
83
+ const own = (wind as Record<string, unknown>)[name];
84
+ if (spec.kind === "enum")
85
+ return {
86
+ kind: "choice",
87
+ label: name,
88
+ value: own === undefined ? "preset" : String(own),
89
+ options: ["preset", ...(spec as EnumParam).values],
90
+ command: (option) =>
91
+ option === "preset" ? `wind ${name} unset` : `wind ${name} ${option}`,
92
+ help: `${spec.doc} · preset ${String(fallback[name])}`,
93
+ };
94
+ if (spec.kind === "boolean")
95
+ return {
96
+ kind: "choice",
97
+ label: name,
98
+ value: own === undefined ? "preset" : own ? "on" : "off",
99
+ options: ["preset", "on", "off"],
100
+ command: (option) =>
101
+ option === "preset" ? `wind ${name} unset` : `wind ${name} ${option}`,
102
+ help: `${spec.doc} · preset ${fallback[name] ? "on" : "off"}`,
103
+ };
104
+ return numberRow(
105
+ name,
106
+ spec,
107
+ Number(resolved[name]),
108
+ Number(fallback[name]),
109
+ );
110
+ };
111
+ const nodes: MenuNode[] = [
112
+ {
113
+ kind: "choice",
114
+ label: "preset",
115
+ value: wind.preset ?? DEFAULT_WIND_PRESET,
116
+ options: WIND_PRESET_NAMES,
117
+ command: (option) => `wind preset ${option}`,
118
+ help: "the instrument: bore, exciter, range and mute",
119
+ },
120
+ ];
121
+ for (const name of WIND_SIMPLE) {
122
+ const node = row(name);
123
+ if (node) nodes.push(node);
124
+ }
125
+ const rest = Object.keys(WIND_PARAMS).filter(
126
+ (name) => !(WIND_SIMPLE as readonly string[]).includes(name),
127
+ );
128
+ nodes.push({
129
+ kind: "menu",
130
+ id: "wind:advanced",
131
+ label: "advanced",
132
+ detail: rest.join(", "),
133
+ help: "every wind parameter",
134
+ build: () => rest.flatMap((name) => row(name) ?? []),
135
+ });
136
+ if (Object.keys(wind).some((key) => key !== "preset"))
137
+ nodes.push({
138
+ kind: "action",
139
+ label: "reset to preset",
140
+ command: "wind reset",
141
+ help: "clear overrides, keep the preset",
142
+ });
143
+ return nodes;
144
+ }
package/src/version.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The package version, read from package.json by a static JSON import so
3
+ * `bun build --compile` embeds it: a runtime read relative to
4
+ * import.meta.url finds nothing inside a compiled binary.
5
+ */
6
+ import pkg from "../package.json" with { type: "json" };
7
+
8
+ export const VERSION: string = pkg.version;
package/src/web/fetch.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { lookup as dnsLookup } from "node:dns/promises";
9
9
  import { isIP } from "node:net";
10
+ import { checkServerIdentity, type PeerCertificate } from "node:tls";
10
11
  import {
11
12
  decodeEntities,
12
13
  describeFetchError,
@@ -81,26 +82,64 @@ function isPrivateV4(address: string): boolean {
81
82
  );
82
83
  }
83
84
 
84
- function isPrivateV6(address: string): boolean {
85
- const lower = address.toLowerCase().split("%")[0]!;
86
- if (lower === "::" || lower === "::1") return true;
87
- const mapped = /^(?:0*:)*:?ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
88
- if (mapped) return isPrivateV4(mapped[1]!);
89
- const hexMapped = /^(?:0*:)*:?ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(
90
- lower,
91
- );
92
- if (hexMapped) {
93
- const high = Number.parseInt(hexMapped[1]!, 16);
94
- const low = Number.parseInt(hexMapped[2]!, 16);
95
- return isPrivateV4(`${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`);
85
+ /** Expand an IPv6 address (zone stripped, embedded dotted IPv4 allowed) to 8 groups. */
86
+ function expandV6(address: string): number[] | undefined {
87
+ let text = address.toLowerCase().split("%")[0]!;
88
+ const dotted = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text);
89
+ if (dotted) {
90
+ const [a, b, c, d] = dotted.slice(1).map(Number) as [
91
+ number,
92
+ number,
93
+ number,
94
+ number,
95
+ ];
96
+ text = `${text.slice(0, dotted.index)}${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;
96
97
  }
97
- const first = Number.parseInt(lower.split(":")[0] || "0", 16);
98
+ const halves = text.split("::");
99
+ if (halves.length > 2) return undefined;
100
+ const parse = (part: string) =>
101
+ part === ""
102
+ ? []
103
+ : part.split(":").map((group) => Number.parseInt(group, 16));
104
+ const head = parse(halves[0]!);
105
+ const tail = halves.length === 2 ? parse(halves[1]!) : [];
106
+ const fill = 8 - head.length - tail.length;
107
+ if (halves.length === 1 ? fill !== 0 : fill < 1) return undefined;
108
+ const groups = [
109
+ ...head,
110
+ ...new Array<number>(halves.length === 2 ? fill : 0).fill(0),
111
+ ...tail,
112
+ ];
113
+ return groups.every((g) => Number.isInteger(g) && g >= 0 && g <= 0xffff)
114
+ ? groups
115
+ : undefined;
116
+ }
117
+
118
+ function v4From(high: number, low: number): string {
119
+ return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
120
+ }
121
+
122
+ function isPrivateV6(address: string): boolean {
123
+ const g = expandV6(address);
124
+ if (!g) return true;
125
+ const zero = (from: number, to: number) =>
126
+ g.slice(from, to).every((x) => x === 0);
127
+ // ::/96 covers :: and ::1 and the deprecated IPv4-compatible form (::a.b.c.d).
128
+ if (zero(0, 6)) return true;
129
+ // ::ffff:0:0/96 IPv4-mapped: as private as the address it carries.
130
+ if (zero(0, 5) && g[5] === 0xffff) return isPrivateV4(v4From(g[6]!, g[7]!));
131
+ // 2002::/16 6to4 embeds an IPv4 address in bits 16..47.
132
+ if (g[0] === 0x2002) return isPrivateV4(v4From(g[1]!, g[2]!));
133
+ const first = g[0]!;
98
134
  return (
135
+ (first === 0x64 && g[1] === 0xff9b) || // 64:ff9b::/96 and 64:ff9b:1::/48 NAT64
136
+ (first === 0x100 && zero(1, 4)) || // 100::/64 discard
137
+ (first === 0x2001 && g[1] === 0) || // 2001::/32 Teredo (obfuscated IPv4)
138
+ (first === 0x2001 && g[1] === 0xdb8) || // 2001:db8::/32 documentation
99
139
  (first & 0xfe00) === 0xfc00 || // fc00::/7 unique local
100
140
  (first & 0xffc0) === 0xfe80 || // fe80::/10 link-local
101
- (first & 0xff00) === 0xff00 || // ff00::/8 multicast
102
- lower.startsWith("64:ff9b:") || // NAT64 well-known prefix
103
- lower.startsWith("2001:db8:") // documentation
141
+ (first & 0xffc0) === 0xfec0 || // fec0::/10 deprecated site-local
142
+ (first & 0xff00) === 0xff00 // ff00::/8 multicast
104
143
  );
105
144
  }
106
145
 
@@ -136,12 +175,19 @@ export function admitUrl(raw: unknown): URL {
136
175
  return url;
137
176
  }
138
177
 
139
- async function assertPublicHost(url: URL, lookup: Lookup): Promise<void> {
178
+ /**
179
+ * Check that a URL's host is public and return the address to connect to.
180
+ * Every resolved address must be public; the first one is returned so the
181
+ * request connects to exactly the address that was checked, never to a
182
+ * second, independent DNS answer (which a rebinding domain could point at
183
+ * loopback or a metadata service).
184
+ */
185
+ async function assertPublicHost(url: URL, lookup: Lookup): Promise<string> {
140
186
  const host = url.hostname.replace(/^\[|\]$/g, "");
141
187
  if (isIP(host)) {
142
188
  if (isPrivateAddress(host))
143
189
  throw new WebError(`${host} is a private or local address`);
144
- return;
190
+ return host;
145
191
  }
146
192
  let addresses: readonly string[];
147
193
  try {
@@ -153,6 +199,42 @@ async function assertPublicHost(url: URL, lookup: Lookup): Promise<void> {
153
199
  for (const address of addresses)
154
200
  if (isPrivateAddress(address))
155
201
  throw new WebError(`${host} resolves to a private or local address`);
202
+ return addresses[0]!;
203
+ }
204
+
205
+ /** Bun's fetch accepts TLS options beyond the standard RequestInit. */
206
+ type PinnedInit = RequestInit & {
207
+ tls?: {
208
+ serverName?: string;
209
+ checkServerIdentity?: (
210
+ hostname: string,
211
+ cert: PeerCertificate,
212
+ ) => Error | undefined;
213
+ };
214
+ };
215
+
216
+ /**
217
+ * Address a request to a checked IP while keeping the original name for the
218
+ * Host header and for TLS (SNI and certificate verification), so the
219
+ * connection cannot be redirected by a later DNS answer.
220
+ */
221
+ export function pinRequest(
222
+ url: URL,
223
+ address: string,
224
+ ): { target: string; host: string; tls?: PinnedInit["tls"] } {
225
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
226
+ const pinned = new URL(url.toString());
227
+ pinned.hostname = isIP(address) === 6 ? `[${address}]` : address;
228
+ const result: { target: string; host: string; tls?: PinnedInit["tls"] } = {
229
+ target: pinned.toString(),
230
+ host: url.host,
231
+ };
232
+ if (url.protocol === "https:" && !isIP(hostname))
233
+ result.tls = {
234
+ serverName: hostname,
235
+ checkServerIdentity: (_name, cert) => checkServerIdentity(hostname, cert),
236
+ };
237
+ return result;
156
238
  }
157
239
 
158
240
  export async function fetchUrl(
@@ -168,18 +250,22 @@ export async function fetchUrl(
168
250
  let url = admitUrl(raw);
169
251
  let response: Response | undefined;
170
252
  for (let hop = 0; hop <= FETCH_LIMITS.maxRedirects; hop += 1) {
171
- await assertPublicHost(url, lookup);
253
+ const address = await assertPublicHost(url, lookup);
254
+ const pin = pinRequest(url, address);
255
+ const init: PinnedInit = {
256
+ method: "GET",
257
+ redirect: "manual",
258
+ headers: {
259
+ host: pin.host,
260
+ accept:
261
+ "text/html,application/xhtml+xml,text/plain;q=0.9,application/json;q=0.8,*/*;q=0.5",
262
+ "user-agent": "Mozilla/5.0 (compatible; dawg-agent)",
263
+ },
264
+ signal: timer.signal,
265
+ };
266
+ if (pin.tls) init.tls = pin.tls;
172
267
  try {
173
- response = await fetcher(url, {
174
- method: "GET",
175
- redirect: "manual",
176
- headers: {
177
- accept:
178
- "text/html,application/xhtml+xml,text/plain;q=0.9,application/json;q=0.8,*/*;q=0.5",
179
- "user-agent": "Mozilla/5.0 (compatible; dawg-agent)",
180
- },
181
- signal: timer.signal,
182
- });
268
+ response = await fetcher(pin.target, init);
183
269
  } catch (error) {
184
270
  throw new WebError(
185
271
  `fetch failed: ${timer.timedOut() ? `timed out after ${timeoutMs / 1000}s` : describeFetchError(error)}`,
package/tui/activity.ts CHANGED
@@ -23,6 +23,33 @@ export interface ActivityCard {
23
23
  trackId?: string | undefined;
24
24
  /** Key hint shown after the card, e.g. "u undo". */
25
25
  hint?: string | undefined;
26
+ /**
27
+ * A one-time info card (a launch note, a tip): it leaves the strip after
28
+ * `ONCE_TTL_MS` or `ONCE_ACTIONS` later actions, whichever comes first.
29
+ */
30
+ once?: boolean | undefined;
31
+ /** Repeats of the same text and tone, shown as `×N` (absent is 1). */
32
+ count?: number | undefined;
33
+ /** A quieter note joined to the card, e.g. the auto-name. */
34
+ suffix?: string | undefined;
35
+ /** `actions` when the card was pushed, for the `once` expiry. */
36
+ atAction?: number | undefined;
37
+ }
38
+
39
+ /** A `once` card's lifetime in milliseconds. */
40
+ export const ONCE_TTL_MS = 8_000;
41
+ /** Actions (receipts, requests) after which a `once` card leaves. */
42
+ export const ONCE_ACTIONS = 3;
43
+ /** Age after which an older error fades to the faint role. */
44
+ export const ERROR_FADE_MS = 6_000;
45
+
46
+ /** One strip line: newlines read as ` · `, runs of space collapse. */
47
+ export function stripLine(text: string): string {
48
+ return text
49
+ .split(/\s*\n\s*/)
50
+ .map((part) => part.replace(/[ \t]+/g, " ").trim())
51
+ .filter(Boolean)
52
+ .join(" · ");
26
53
  }
27
54
 
28
55
  export type TranscriptKind =
@@ -134,7 +161,10 @@ export class ActivityFeed {
134
161
  private spinnerLabel: string | undefined;
135
162
  private spinnerSince = 0;
136
163
  private streamText = "";
164
+ private turnReceipt = "";
137
165
  private queue = 0;
166
+ /** Receipts and requests so far: the clock `once` cards expire on. */
167
+ private actions = 0;
138
168
  private listeners = new Set<() => void>();
139
169
  /** Increments on every change; renderers can skip work when unchanged. */
140
170
  version = 0;
@@ -167,6 +197,36 @@ export class ActivityFeed {
167
197
  return this.cardList[this.cardList.length - 1];
168
198
  }
169
199
 
200
+ /**
201
+ * The cards the strip shows at `nowMs`, first slot first: the newest
202
+ * receipt leads, live `once` notes follow it, and expired `once` notes
203
+ * are gone.
204
+ */
205
+ visible(nowMs: number = this.clock()): ActivityCard[] {
206
+ const live = this.cardList.filter(
207
+ (card) =>
208
+ !card.once ||
209
+ (nowMs - card.atMs < ONCE_TTL_MS &&
210
+ this.actions - (card.atAction ?? 0) < ONCE_ACTIONS),
211
+ );
212
+ const newest = [...live].reverse();
213
+ return [
214
+ ...newest.filter((card) => !card.once),
215
+ ...newest.filter((card) => card.once),
216
+ ];
217
+ }
218
+
219
+ /** The next time a `once` card expires on the clock, for frame wakeups. */
220
+ nextExpiry(nowMs: number = this.clock()): number | undefined {
221
+ let next: number | undefined;
222
+ for (const card of this.cardList) {
223
+ if (!card.once) continue;
224
+ const at = card.atMs + ONCE_TTL_MS;
225
+ if (at > nowMs && (next === undefined || at < next)) next = at;
226
+ }
227
+ return next;
228
+ }
229
+
170
230
  subscribe(listener: () => void): () => void {
171
231
  this.listeners.add(listener);
172
232
  return () => this.listeners.delete(listener);
@@ -176,32 +236,85 @@ export class ActivityFeed {
176
236
  text: string,
177
237
  details: Omit<Partial<ActivityCard>, "id" | "atMs" | "text"> = {},
178
238
  ): ActivityCard {
239
+ const line = stripLine(text);
240
+ const once = details.once === true;
241
+ if (!once) this.actions += 1;
242
+ const last = this.cardList[this.cardList.length - 1];
243
+ const tone = details.tone ?? "info";
244
+ // The same message again reads `×N` instead of filling the strip.
245
+ if (
246
+ last &&
247
+ last.text === line &&
248
+ last.tone === tone &&
249
+ last.resultRevision === details.resultRevision
250
+ ) {
251
+ const repeat: ActivityCard = {
252
+ ...last,
253
+ count: (last.count ?? 1) + 1,
254
+ atMs: this.clock(),
255
+ atAction: this.actions,
256
+ };
257
+ this.cardList = [...this.cardList.slice(0, -1), repeat];
258
+ this.record(tone === "error" ? "error" : "op", line);
259
+ this.changed();
260
+ return repeat;
261
+ }
179
262
  const card: ActivityCard = {
180
263
  id: this.nextId++,
181
- text,
182
- tone: details.tone ?? "info",
264
+ text: line,
265
+ tone,
183
266
  atMs: this.clock(),
184
267
  baseRevision: details.baseRevision,
185
268
  resultRevision: details.resultRevision,
186
269
  trackId: details.trackId,
187
270
  hint: details.hint,
271
+ ...(once ? { once: true } : {}),
272
+ ...(details.suffix ? { suffix: stripLine(details.suffix) } : {}),
273
+ atAction: this.actions,
188
274
  };
189
275
  this.cardList = [...this.cardList, card].slice(-this.maxCards);
190
276
  const revision = revisionLabel(card.baseRevision, card.resultRevision);
191
277
  this.record(
192
278
  card.tone === "error" ? "error" : "op",
193
- revision ? `${text} · ${revision}` : text,
279
+ revision ? `${line} · ${revision}` : line,
194
280
  );
195
281
  this.changed();
196
282
  return card;
197
283
  }
198
284
 
285
+ /**
286
+ * Join a quiet note to the newest receipt (`… · named "dusk loop"`), so
287
+ * it rides in the first slot instead of pushing the receipt aside. With
288
+ * no receipt in the last few seconds it becomes a `once` card.
289
+ */
290
+ attachNote(text: string, withinMs = ONCE_TTL_MS): ActivityCard {
291
+ const line = stripLine(text);
292
+ const last = this.cardList[this.cardList.length - 1];
293
+ if (
294
+ last &&
295
+ !last.once &&
296
+ last.tone !== "error" &&
297
+ this.clock() - last.atMs < withinMs
298
+ ) {
299
+ const joined: ActivityCard = {
300
+ ...last,
301
+ suffix: last.suffix ? `${last.suffix} · ${line}` : line,
302
+ };
303
+ this.cardList = [...this.cardList.slice(0, -1), joined];
304
+ this.record("note", line);
305
+ this.changed();
306
+ return joined;
307
+ }
308
+ return this.pushCard(line, { tone: "info", once: true });
309
+ }
310
+
199
311
  pushError(message: string): ActivityCard {
200
312
  return this.pushCard(message, { tone: "error" });
201
313
  }
202
314
 
203
315
  /** Record a user request in the transcript without adding a strip card. */
204
316
  pushRequest(text: string): void {
317
+ this.actions += 1;
205
318
  this.record("request", text);
206
319
  this.changed();
207
320
  }
@@ -227,6 +340,23 @@ export class ActivityFeed {
227
340
  this.changed();
228
341
  }
229
342
 
343
+ /**
344
+ * The musical receipt of the running agent turn (`musicalReceipt` of the
345
+ * score before and after), shown as the turn's closing card.
346
+ */
347
+ setTurnReceipt(text: string): void {
348
+ this.turnReceipt = text;
349
+ }
350
+
351
+ /**
352
+ * The latest prose sentence of the streaming agent text, for the faint
353
+ * tail of the spinner: a complete sentence when one has ended, else the
354
+ * one being written.
355
+ */
356
+ get streamSentence(): string {
357
+ return latestSentence(this.streamText);
358
+ }
359
+
230
360
  /** Adapter for the agent lane's streaming events. */
231
361
  applyAgentEvent(event: AgentActivityEvent): void {
232
362
  switch (event.type) {
@@ -271,7 +401,7 @@ export class ActivityFeed {
271
401
  baseRevision: event.baseRevision,
272
402
  resultRevision: event.resultRevision,
273
403
  trackId: event.trackId,
274
- hint: unchanged ? undefined : "^z undo",
404
+ hint: unchanged ? undefined : "ctrl-z undo",
275
405
  });
276
406
  return;
277
407
  }
@@ -287,9 +417,20 @@ export class ActivityFeed {
287
417
  case "done": {
288
418
  if (this.streamText) this.record("agent", this.streamText);
289
419
  this.streamText = "";
420
+ // A turn that changed the music ends on what it changed, in musical
421
+ // terms; its prose stays in the ctrl-o log.
422
+ const receipt = this.turnReceipt;
423
+ this.turnReceipt = "";
290
424
  const summary =
291
- event.summary ?? event.text?.split("\n")[0]?.slice(0, 160);
292
- if (summary) this.pushCard(summary, { tone: "agent" });
425
+ receipt ||
426
+ event.summary ||
427
+ firstSentence(event.text ?? "").slice(0, 160) ||
428
+ undefined;
429
+ if (summary)
430
+ this.pushCard(summary, {
431
+ tone: "agent",
432
+ hint: receipt ? "ctrl-z undo" : undefined,
433
+ });
293
434
  else if (event.applied === 0)
294
435
  this.pushCard("agent made no changes", { tone: "info" });
295
436
  this.setSpinner(undefined);
@@ -300,7 +441,7 @@ export class ActivityFeed {
300
441
  this.setSpinner(undefined);
301
442
  if (event.code === "aborted")
302
443
  this.pushCard(
303
- `cancelled${event.revision === undefined ? "" : ` · kept rev ${event.revision}`}`,
444
+ `canceled${event.revision === undefined ? "" : ` · kept rev ${event.revision}`}`,
304
445
  { tone: "warning" },
305
446
  );
306
447
  else
@@ -331,11 +472,16 @@ export class ActivityFeed {
331
472
  * success, `false` a failure, `"warn"` a no-op or conflict. Handlers return
332
473
  * these so the strip never infers failure from prose.
333
474
  */
334
- export type Receipt = Readonly<{ ok: boolean | "warn"; text: string }>;
475
+ export type Receipt = Readonly<{
476
+ ok: boolean | "warn" | "info";
477
+ text: string;
478
+ }>;
335
479
 
336
480
  export const ok = (text: string): Receipt => ({ ok: true, text });
337
481
  export const fail = (text: string): Receipt => ({ ok: false, text });
338
482
  export const warn = (text: string): Receipt => ({ ok: "warn", text });
483
+ /** A status read (`•`): nothing changed (bare `fx`, `tracks`). */
484
+ export const note = (text: string): Receipt => ({ ok: "info", text });
339
485
 
340
486
  /** Tone of a structured receipt; strings fall back to `receiptTone`. */
341
487
  export function toneOf(receipt: string | Receipt): CardTone {
@@ -344,7 +490,9 @@ export function toneOf(receipt: string | Receipt): CardTone {
344
490
  ? "success"
345
491
  : receipt.ok === "warn"
346
492
  ? "warning"
347
- : "error";
493
+ : receipt.ok === "info"
494
+ ? "info"
495
+ : "error";
348
496
  }
349
497
 
350
498
  /**
@@ -362,3 +510,26 @@ export function receiptTone(message: string): CardTone {
362
510
  return "warning";
363
511
  return "success";
364
512
  }
513
+
514
+ /** The first sentence of a reply (up to its first `.`, `!` or `?`). */
515
+ export function firstSentence(text: string): string {
516
+ const flat = text.replace(/\s+/g, " ").trim();
517
+ const match = /^.*?[.!?](?=\s|$)/.exec(flat);
518
+ return (match ? match[0] : flat).trim();
519
+ }
520
+
521
+ /** The last complete sentence of streaming text, or the one in progress. */
522
+ export function latestSentence(text: string): string {
523
+ const flat = text.replace(/\s+/g, " ").trim();
524
+ if (!flat) return "";
525
+ const sentences = flat.match(/[^.!?]+[.!?]+(?=\s|$)|[^.!?]+$/g) ?? [flat];
526
+ const last = sentences[sentences.length - 1]!.trim();
527
+ // A fragment of a few words reads as noise; keep the sentence before it.
528
+ if (
529
+ !/[.!?]$/.test(last) &&
530
+ last.split(" ").length < 4 &&
531
+ sentences.length > 1
532
+ )
533
+ return sentences[sentences.length - 2]!.trim();
534
+ return last;
535
+ }