opencharm 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -18,7 +18,7 @@ npx opencharm
18
18
  | `opencharm sim` | The charm on screen, in your browser, talking to charmd (`--url`, `--port`, `--no-open`) |
19
19
  | `sudo opencharm setup --domain <d>` | Install charmd as a hardened systemd service on a Linux server (`--dry-run` prints the plan; see `docs/deploy.md`) |
20
20
  | `opencharm pair <code>` | Pair the charm showing that code; you choose its PIN (`--name <name>`) |
21
- | `opencharm status` | Paired charms and whether they are connected, locked or blocked |
21
+ | `opencharm status` | What's running (charmd, each connected charm's build, the starter commit of the workspace; spec 015), then the paired charms and whether they are connected, locked or blocked |
22
22
  | `opencharm lock` / `unlock` / `revoke <charm>` | Lock it now; clear a block and unlock; forget it |
23
23
  | `opencharm look` | Show the charm's look, or change it on every unlocked charm until charmd restarts (`--colour <colour>`, `--greeting <text>`, `--motion full\|calm`); to keep it, set `charm` in `opencharm.json` |
24
24
  | `opencharm dev face <charm> <state> [text]` / `dev say <charm> <text>` / `dev ask <charm> <question>` | Show a face, speak, or ask a question (hold = yes, press = no) on a connected charm (testing) |
@@ -30,6 +30,6 @@ Node 24 or newer. Runtime dependencies: `ws`, `zod`.
30
30
 
31
31
  Source and hardware: <https://github.com/opencharm-labs/opencharm>
32
32
 
33
- Development: this package lives in the OpenCharm monorepo (`packages/cli`). Run it from the repo root with `npm run cli -- <command>`; `npm run build -w packages/cli` bundles `dist/main.mjs`.
33
+ Development: this package lives in the OpenCharm monorepo (`packages/cli`). Run it from the repo root with `npm run cli -- <command>`; `npm run build -w packages/cli` bundles `dist/main.mjs`. `opencharm --version` prints `cli@<version> (<commit>)` (spec 015); from source it says `cli@0.0.0`: released versions come from their `cli@` tag, and each merged `fix`/`feat`/`perf` that touches the CLI publishes one (CONTRIBUTING, "Releasing").
34
34
 
35
35
  Not affiliated with Meta, Nous Research, OpenClaw, Waveshare or Espressif.
package/dist/main.mjs CHANGED
@@ -5,7 +5,7 @@ import { dirname, extname, isAbsolute, join, normalize, resolve, sep } from "nod
5
5
  import { z } from "zod";
6
6
  import { createConnection, createServer } from "node:net";
7
7
  import { createInterface } from "node:readline";
8
- import { execFile, spawn, spawnSync } from "node:child_process";
8
+ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
9
9
  import { createHash, randomBytes, randomInt, randomUUID, scrypt, timingSafeEqual } from "node:crypto";
10
10
  import { createServer as createServer$1 } from "node:http";
11
11
  import { WebSocketServer } from "ws";
@@ -455,12 +455,20 @@ const audioParams = z.object({
455
455
  channels: z.number().int().min(1).max(2),
456
456
  frame_duration: z.number().int().positive()
457
457
  });
458
+ const plain = (max) => z.string().min(1).max(max).regex(/^[\w.@+-]+$/);
459
+ const charmBuild = z.object({
460
+ kind: plain(16),
461
+ version: plain(64),
462
+ commit: plain(40)
463
+ });
464
+ const build = charmBuild.optional().catch(void 0);
458
465
  const clientHello = z.object({
459
466
  type: z.literal("hello"),
460
467
  version: z.number().int().positive(),
461
468
  features: z.record(z.string(), z.boolean()).optional(),
462
469
  transport: z.literal("websocket"),
463
- audio_params: audioParams
470
+ audio_params: audioParams,
471
+ build
464
472
  });
465
473
  const listen = z.object({
466
474
  session_id: sessionId,
@@ -858,7 +866,7 @@ function sendAdmin(socketPath, request) {
858
866
  }
859
867
  //#endregion
860
868
  //#region package.json
861
- var version = "0.1.0";
869
+ var version = "0.2.0";
862
870
  function channelToCube(value) {
863
871
  if (value < 48) return 0;
864
872
  if (value < 115) return 1;
@@ -999,6 +1007,17 @@ function promptHidden(question) {
999
1007
  }
1000
1008
  //#endregion
1001
1009
  //#region src/commands/admin.ts
1010
+ const statusSchema = z.object({
1011
+ charmd: z.string().regex(/^[\w.@+() -]{1,96}$/).optional().catch(void 0),
1012
+ starter: z.string().regex(/^[0-9a-f]{4,40}$/).optional().catch(void 0),
1013
+ charms: z.array(z.object({
1014
+ name: z.string(),
1015
+ state: z.string(),
1016
+ blocked: z.boolean(),
1017
+ failedTries: z.number(),
1018
+ build: charmBuild.optional().catch(void 0)
1019
+ }))
1020
+ });
1002
1021
  const DONE = {
1003
1022
  lock: "Locked",
1004
1023
  unlock: "Unblocked and unlocked",
@@ -1083,14 +1102,16 @@ async function look(ctx, args, deps) {
1083
1102
  if (Object.keys(update).length === 0) return;
1084
1103
  ctx.out.write(`${result.connected === 0 ? "No charm is unlocked right now: it gets the look when it unlocks." : `Sent to ${result.connected} charm${result.connected === 1 ? "" : "s"}.`} It lasts until charmd restarts; to keep it, set "charm" in opencharm.json.\n`);
1085
1104
  }
1086
- function printStatus(ctx, charms) {
1105
+ function printStatus(ctx, charms, charmd, starter) {
1106
+ if (charmd) ctx.out.write(`charmd ${charmd}\n`);
1107
+ if (starter) ctx.out.write(`workspace from starter ${starter}\n`);
1087
1108
  if (charms.length === 0) {
1088
1109
  ctx.out.write("No charms paired yet. Start one and run: opencharm pair <code>\n");
1089
1110
  return;
1090
1111
  }
1091
1112
  for (const charm of charms) {
1092
- const note = charm.blocked ? "blocked (opencharm unlock to clear)" : charm.failedTries > 0 ? `${charm.failedTries} wrong PIN(s)` : "";
1093
- ctx.out.write(`${charm.name.padEnd(16)} ${charm.state.padEnd(10)} ${note}\n`.trimEnd() + "\n");
1113
+ const details = [charm.blocked ? "blocked (opencharm unlock to clear)" : charm.failedTries > 0 ? `${charm.failedTries} wrong PIN(s)` : "", charm.build ? `${charm.build.kind} ${charm.build.version} (${charm.build.commit})` : ""].filter(Boolean).join(" ");
1114
+ ctx.out.write(`${charm.name.padEnd(16)} ${charm.state.padEnd(10)} ${details}`.trimEnd() + "\n");
1094
1115
  }
1095
1116
  }
1096
1117
  async function runAdminCommand(ctx, command, args, deps) {
@@ -1108,8 +1129,13 @@ async function runAdminCommand(ctx, command, args, deps) {
1108
1129
  return;
1109
1130
  }
1110
1131
  if (command === "status") {
1111
- const { charms } = await deps.send(deps.socket, { cmd: "status" });
1112
- printStatus(ctx, charms);
1132
+ const answer = statusSchema.safeParse(await deps.send(deps.socket, { cmd: "status" }));
1133
+ if (!answer.success) {
1134
+ fail$2(ctx, "Unexpected answer from charmd: is it the same version as this CLI?");
1135
+ return;
1136
+ }
1137
+ const { charms, charmd, starter } = answer.data;
1138
+ printStatus(ctx, charms, charmd, starter);
1113
1139
  return;
1114
1140
  }
1115
1141
  const charm = firstPositional(args);
@@ -1705,7 +1731,7 @@ function parseInput(schema, input) {
1705
1731
  if (!result.success) throw new Error(result.error.issues.map((i) => i.message).join("; "));
1706
1732
  return result.data;
1707
1733
  }
1708
- function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook }) {
1734
+ function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook, identity, starter }) {
1709
1735
  function sessionsOf(charmId) {
1710
1736
  return [...sessions()].filter((s) => s.charmId === charmId && s.state !== "closed");
1711
1737
  }
@@ -1811,17 +1837,22 @@ function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook
1811
1837
  });
1812
1838
  },
1813
1839
  status() {
1814
- return { charms: store.list().map((charm) => {
1815
- const live = sessionsOf(charm.id).at(-1);
1816
- return {
1817
- id: charm.id,
1818
- name: charm.name,
1819
- blocked: charm.blocked,
1820
- failedTries: charm.failedTries,
1821
- connected: live !== void 0,
1822
- state: live?.state ?? "offline"
1823
- };
1824
- }) };
1840
+ return {
1841
+ charmd: identity ?? "unknown",
1842
+ ...starter ? { starter } : {},
1843
+ charms: store.list().map((charm) => {
1844
+ const live = sessionsOf(charm.id).at(-1);
1845
+ return {
1846
+ id: charm.id,
1847
+ name: charm.name,
1848
+ blocked: charm.blocked,
1849
+ failedTries: charm.failedTries,
1850
+ connected: live !== void 0,
1851
+ state: live?.state ?? "offline",
1852
+ build: live?.build
1853
+ };
1854
+ })
1855
+ };
1825
1856
  }
1826
1857
  };
1827
1858
  }
@@ -2085,6 +2116,7 @@ var Session = class {
2085
2116
  sessionId = randomBytes(8).toString("hex");
2086
2117
  #state = "awaiting_hello";
2087
2118
  #charmId;
2119
+ #build;
2088
2120
  #queue = Promise.resolve();
2089
2121
  #turn;
2090
2122
  #asks = Promise.resolve();
@@ -2097,6 +2129,9 @@ var Session = class {
2097
2129
  get state() {
2098
2130
  return this.#state;
2099
2131
  }
2132
+ get build() {
2133
+ return this.#build;
2134
+ }
2100
2135
  get charmId() {
2101
2136
  return this.#charmId;
2102
2137
  }
@@ -2181,6 +2216,7 @@ var Session = class {
2181
2216
  this.#close(CLOSE_POLICY$1);
2182
2217
  return;
2183
2218
  }
2219
+ this.#build = message.build;
2184
2220
  this.#onHello();
2185
2221
  return;
2186
2222
  }
@@ -3612,7 +3648,9 @@ async function startDaemon(config, options = {}) {
3612
3648
  pairing,
3613
3649
  sessions: () => sessions.values(),
3614
3650
  look,
3615
- agentCanChangeLook
3651
+ agentCanChangeLook,
3652
+ identity: options.identity,
3653
+ starter: options.starter
3616
3654
  });
3617
3655
  const adminServer = await startAdminServer(config.adminSocket, admin);
3618
3656
  let websocketUrl = config.publicUrl ?? "";
@@ -3699,7 +3737,12 @@ async function startDaemon(config, options = {}) {
3699
3737
  const { port } = http.address();
3700
3738
  const url = `ws://${config.listen.host.includes(":") ? `[${config.listen.host}]` : config.listen.host}:${port}/charm`;
3701
3739
  websocketUrl = config.publicUrl ?? url;
3702
- if (!options.quiet) console.log(`charmd listening on ${url}`);
3740
+ if (!options.quiet) console.log([
3741
+ "charmd",
3742
+ options.identity,
3743
+ "listening on",
3744
+ url
3745
+ ].filter(Boolean).join(" "));
3703
3746
  return {
3704
3747
  url,
3705
3748
  admin,
@@ -3714,14 +3757,53 @@ async function startDaemon(config, options = {}) {
3714
3757
  };
3715
3758
  }
3716
3759
  //#endregion
3760
+ //#region src/identity.ts
3761
+ function git(cwd, ...args) {
3762
+ return execFileSync("git", args, {
3763
+ cwd,
3764
+ encoding: "utf8",
3765
+ stdio: [
3766
+ "ignore",
3767
+ "pipe",
3768
+ "ignore"
3769
+ ]
3770
+ }).trim();
3771
+ }
3772
+ function starterCommit(dir) {
3773
+ try {
3774
+ if (!existsSync(join(git(dir, "rev-parse", "--show-toplevel"), "opencharm.json"))) return void 0;
3775
+ return git(dir, "rev-parse", "--short", git(dir, "merge-base", "HEAD", "upstream/HEAD"));
3776
+ } catch {
3777
+ return;
3778
+ }
3779
+ }
3780
+ function formatIdentity(unit, version, commit) {
3781
+ return `${unit}@${version} (${commit})`;
3782
+ }
3783
+ let cached;
3784
+ function identity() {
3785
+ if (cached) return cached;
3786
+ const commit = "f4edfa0";
3787
+ cached = {
3788
+ version: `cli@${version}`,
3789
+ commit,
3790
+ text: formatIdentity("cli", version, commit)
3791
+ };
3792
+ return cached;
3793
+ }
3794
+ //#endregion
3717
3795
  //#region src/commands/serve.ts
3718
3796
  async function runServe(ctx, args) {
3719
3797
  let config;
3720
3798
  let daemon;
3721
3799
  try {
3722
- config = loadConfig(flagValue(args, "--config"));
3800
+ const configPath = flagValue(args, "--config");
3801
+ config = loadConfig(configPath);
3802
+ const workspace = configPath ? dirname(resolve(configPath)) : existsSync("opencharm.json") ? process.cwd() : void 0;
3723
3803
  daemon = await startDaemon(config, {
3724
3804
  quiet: true,
3805
+ identity: identity().text,
3806
+ starter: workspace ? starterCommit(workspace) : void 0,
3725
3807
  charmTools: {
3726
3808
  command: process.execPath,
3727
3809
  args: [
@@ -3740,7 +3822,7 @@ async function runServe(ctx, args) {
3740
3822
  process.exitCode = 1;
3741
3823
  return;
3742
3824
  }
3743
- ctx.out.write(`charmd (the charm daemon) is listening on ${daemon.url}\n`);
3825
+ ctx.out.write(`charmd (the charm daemon) ${identity().text} is listening on ${daemon.url}\n`);
3744
3826
  ctx.out.write(`Admin socket: ${config.adminSocket}\n`);
3745
3827
  ctx.out.write("Pair a charm: opencharm pair <code> Stop: Ctrl-C\n");
3746
3828
  const stop = () => {
@@ -3993,7 +4075,10 @@ function createSimServer(dir, charmdUrl) {
3993
4075
  return;
3994
4076
  }
3995
4077
  if (path === "/config.json") {
3996
- res.writeHead(200, { "content-type": TYPES[".json"] }).end(JSON.stringify({ url: charmdUrl }));
4078
+ res.writeHead(200, { "content-type": TYPES[".json"] }).end(JSON.stringify({
4079
+ url: charmdUrl,
4080
+ version: identity().version
4081
+ }));
3997
4082
  return;
3998
4083
  }
3999
4084
  const file = resolve(root, normalize(`.${path === "/" ? "/index.html" : path}`));
@@ -4086,7 +4171,7 @@ async function main(argv) {
4086
4171
  const ctx = buildContext(argv);
4087
4172
  const { command, rest, help, version } = parseArgs(argv);
4088
4173
  if (version) {
4089
- ctx.out.write(`${ctx.version}\n`);
4174
+ ctx.out.write(`${identity().text}\n`);
4090
4175
  return;
4091
4176
  }
4092
4177
  if (help || command === "help") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencharm",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "An open-source body for the AI agent you already run: a face, a voice, one key. Claude Code, Codex, Gemini CLI, Hermes, OpenClaw or any ACP or OpenAI-compatible agent; on a small board, your Mac's notch or the browser.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,7 @@
24
24
  "devDependencies": {
25
25
  "@opencharm-labs/charmd": "*",
26
26
  "@opencharm-labs/design": "*",
27
+ "@opencharm-labs/protocol": "*",
27
28
  "playwright": "^1.63.0",
28
29
  "tsdown": "^0.23.0"
29
30
  },
package/sim/charm_sim.js CHANGED
@@ -1,2 +1,2 @@
1
- async function createCharm(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var programArgs=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["r"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("charm_sim.wasm")}return new URL("charm_sim.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var HEAPU32;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var __Unwind_RaiseException=ex=>{abort()};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;__Unwind_RaiseException(ptr)};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var HEAPF32;var HEAP16;var HEAP32;var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;function js_panel(open){charmSim.panel(!!open)}function js_send_text(json){charmSim.sendText(UTF8ToString(json))}function js_send_binary(data,size){charmSim.sendBinary(HEAPU8.slice(data,data+size))}function js_mic(on){charmSim.mic(!!on)}function js_play_pcm(pcm,samples){charmSim.playPcm(HEAPF32.slice(pcm>>2,(pcm>>2)+samples))}function js_stop_audio(){charmSim.stopAudio()}function js_brightness(percent){charmSim.brightness(percent)}function js_store_get(key){var value=localStorage.getItem("opencharm."+UTF8ToString(key));if(value===null)return 0;var size=lengthBytesUTF8(value)+1;var ptr=_malloc(size);stringToUTF8(value,ptr,size);return ptr}function js_store_set(key,value){localStorage.setItem("opencharm."+UTF8ToString(key),UTF8ToString(value))}function js_store_erase(key){localStorage.removeItem("opencharm."+UTF8ToString(key))}function js_reconnect(){charmSim.reconnect()}var _sim_init,_sim_frame,_sim_pin_key,_sim_frame_box,_sim_frame_dirty,_sim_tick,_sim_pointer,_sim_key,_sim_connected,_sim_disconnected,_sim_text,_sim_audio,_sim_mic_pcm,_sim_token,_sim_forget,_free,_malloc,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_sim_init=Module["_sim_init"]=wasmExports["s"];_sim_frame=Module["_sim_frame"]=wasmExports["t"];_sim_pin_key=Module["_sim_pin_key"]=wasmExports["u"];_sim_frame_box=Module["_sim_frame_box"]=wasmExports["v"];_sim_frame_dirty=Module["_sim_frame_dirty"]=wasmExports["w"];_sim_tick=Module["_sim_tick"]=wasmExports["x"];_sim_pointer=Module["_sim_pointer"]=wasmExports["y"];_sim_key=Module["_sim_key"]=wasmExports["z"];_sim_connected=Module["_sim_connected"]=wasmExports["A"];_sim_disconnected=Module["_sim_disconnected"]=wasmExports["B"];_sim_text=Module["_sim_text"]=wasmExports["C"];_sim_audio=Module["_sim_audio"]=wasmExports["D"];_sim_mic_pcm=Module["_sim_mic_pcm"]=wasmExports["E"];_sim_token=Module["_sim_token"]=wasmExports["F"];_sim_forget=Module["_sim_forget"]=wasmExports["G"];_free=Module["_free"]=wasmExports["H"];_malloc=Module["_malloc"]=wasmExports["I"];memory=wasmMemory=wasmExports["q"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={o:___cxa_throw,l:__abort_js,j:_emscripten_resize_heap,k:_fd_close,b:_fd_write,e:js_brightness,a:js_mic,p:js_panel,g:js_play_pcm,m:js_reconnect,h:js_send_binary,i:js_send_text,f:js_stop_audio,n:js_store_erase,d:js_store_get,c:js_store_set};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
1
+ async function createCharm(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var programArgs=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["r"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("charm_sim.wasm")}return new URL("charm_sim.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var HEAPU32;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var __Unwind_RaiseException=ex=>{abort()};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;__Unwind_RaiseException(ptr)};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var HEAPF32;var HEAP16;var HEAP32;var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;function js_panel(open){charmSim.panel(!!open)}function js_send_text(json){charmSim.sendText(UTF8ToString(json))}function js_send_binary(data,size){charmSim.sendBinary(HEAPU8.slice(data,data+size))}function js_mic(on){charmSim.mic(!!on)}function js_play_pcm(pcm,samples){charmSim.playPcm(HEAPF32.slice(pcm>>2,(pcm>>2)+samples))}function js_stop_audio(){charmSim.stopAudio()}function js_brightness(percent){charmSim.brightness(percent)}function js_store_get(key){var value=localStorage.getItem("opencharm."+UTF8ToString(key));if(value===null)return 0;var size=lengthBytesUTF8(value)+1;var ptr=_malloc(size);stringToUTF8(value,ptr,size);return ptr}function js_store_set(key,value){localStorage.setItem("opencharm."+UTF8ToString(key),UTF8ToString(value))}function js_store_erase(key){localStorage.removeItem("opencharm."+UTF8ToString(key))}function js_reconnect(){charmSim.reconnect()}var _sim_set_build,_sim_commit,_sim_init,_sim_frame,_sim_pin_key,_sim_frame_box,_sim_frame_dirty,_sim_tick,_sim_pointer,_sim_key,_sim_connected,_sim_disconnected,_sim_text,_sim_audio,_sim_mic_pcm,_sim_token,_sim_forget,_free,_malloc,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_sim_set_build=Module["_sim_set_build"]=wasmExports["s"];_sim_commit=Module["_sim_commit"]=wasmExports["t"];_sim_init=Module["_sim_init"]=wasmExports["u"];_sim_frame=Module["_sim_frame"]=wasmExports["v"];_sim_pin_key=Module["_sim_pin_key"]=wasmExports["w"];_sim_frame_box=Module["_sim_frame_box"]=wasmExports["x"];_sim_frame_dirty=Module["_sim_frame_dirty"]=wasmExports["y"];_sim_tick=Module["_sim_tick"]=wasmExports["z"];_sim_pointer=Module["_sim_pointer"]=wasmExports["A"];_sim_key=Module["_sim_key"]=wasmExports["B"];_sim_connected=Module["_sim_connected"]=wasmExports["C"];_sim_disconnected=Module["_sim_disconnected"]=wasmExports["D"];_sim_text=Module["_sim_text"]=wasmExports["E"];_sim_audio=Module["_sim_audio"]=wasmExports["F"];_sim_mic_pcm=Module["_sim_mic_pcm"]=wasmExports["G"];_sim_token=Module["_sim_token"]=wasmExports["H"];_sim_forget=Module["_sim_forget"]=wasmExports["I"];_free=Module["_free"]=wasmExports["J"];_malloc=Module["_malloc"]=wasmExports["K"];memory=wasmMemory=wasmExports["q"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={o:___cxa_throw,l:__abort_js,j:_emscripten_resize_heap,k:_fd_close,b:_fd_write,e:js_brightness,a:js_mic,p:js_panel,g:js_play_pcm,m:js_reconnect,h:js_send_binary,i:js_send_text,f:js_stop_audio,n:js_store_erase,d:js_store_get,c:js_store_set};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
2
2
  ;return Module}export default createCharm;
Binary file
package/sim/index.html CHANGED
@@ -44,6 +44,8 @@
44
44
  </p>
45
45
  <p class="mono dim">CHARMD</p>
46
46
  <p class="mono small" id="url"></p>
47
+ <p class="mono dim">BUILD</p>
48
+ <p class="mono small" id="build"></p>
47
49
  <div class="buttons">
48
50
  <button class="pill" id="wifi">Drop Wi-Fi</button>
49
51
  <button class="pill" id="shape">Round screen</button>
package/sim/sim.js CHANGED
@@ -122,6 +122,16 @@ window.charmSim = {
122
122
  };
123
123
 
124
124
  const M = await createCharm();
125
+ // What this charm runs (spec 015): the desktop app passes its identity in charmParams, `opencharm sim`
126
+ // in config.json. Shown in the side panel and sent to charmd in hello, never on the charm's screen.
127
+ const version = params.get("version") || config.version || "unknown";
128
+ withString(window.charmParams ? "desktop" : "emulator", (kind) =>
129
+ withString(version, (v) => M._sim_set_build(kind, v))
130
+ );
131
+ // The emulator's page has a BUILD line; the desktop app's page doesn't (its Settings show it).
132
+ const buildLine = document.getElementById("build");
133
+ if (buildLine)
134
+ buildLine.textContent = `${version} (${M.UTF8ToString(M._sim_commit())})`;
125
135
  const glyph = 0;
126
136
  M._sim_init(
127
137
  W,