1// clock — a zot extension written in plain Node (no dependencies).
2//
3// Registers two slash commands:
4// /now — pushes the current local time into the chat as
5// a one-shot note (no model call, no transcript)
6// /uptime — submits a prompt asking the agent to comment on
7// how long this extension has been running
8//
9// Why .js and not .ts: this file uses JSDoc types so it can be
10// type-checked by tsc / tsserver without a build step. Renaming to
11// .ts and updating extension.json's args to ["--import","tsx",
12// "index.ts"] (with tsx installed) works too. The extension protocol
13// itself is language-agnostic; what matters is that `exec` produces
14// a process that reads JSON lines from stdin and writes them to
15// stdout.
16//
17// Install:
18// zot ext install /path/to/this/dir
19//
20// Then in zot:
21// /now
22// /uptime
23
24import { createInterface } from "node:readline";
25import { stdin, stdout, stderr } from "node:process";
26
27const NAME = "clock";
28const VERSION = "1.0.0";
29const STARTED_AT = Date.now();
30
31/** @typedef {{type: string, id?: string, [k: string]: unknown}} Frame */
32
33/**
34 * Send a frame to zot. One JSON object per line; flush immediately
35 * so the host doesn't sit waiting on a buffer.
36 * @param {Frame} obj
37 */
38function send(obj) {
39 stdout.write(JSON.stringify(obj) + "\n");
40}
41
42/**
43 * stderr is captured by zot to $ZOT_HOME/logs/ext-clock.log; perfect
44 * for debug output. Anything written to stdout would corrupt the
45 * protocol stream.
46 * @param {string} msg
47 */
48function log(msg) {
49 stderr.write(`[${NAME}] ${msg}\n`);
50}
51
52// 1. Hello first.
53send({
54 type: "hello",
55 name: NAME,
56 version: VERSION,
57 capabilities: ["commands"],
58});
59
60// 2. Register every command we can handle.
61send({
62 type: "register_command",
63 name: "now",
64 description: "show the current local time (no model call)",
65});
66send({
67 type: "register_command",
68 name: "uptime",
69 description: "ask the agent to riff on how long the clock ext has run",
70});
71
72// 3. Read frames until stdin closes (zot shuts us down).
73const rl = createInterface({ input: stdin, crlfDelay: Infinity });
74
75rl.on("line", (line) => {
76 /** @type {Frame} */
77 let frame;
78 try {
79 frame = JSON.parse(line);
80 } catch (err) {
81 log(`malformed frame: ${err}`);
82 return;
83 }
84
85 switch (frame.type) {
86 case "hello_ack":
87 log(
88 `connected to zot ${frame.zot_version} (${frame.provider}/${frame.model})`,
89 );
90 break;
91
92 case "command_invoked":
93 handleCommand(frame);
94 break;
95
96 case "shutdown":
97 send({ type: "shutdown_ack" });
98 rl.close();
99 break;
100
101 default:
102 log(`unknown frame type: ${frame.type}`);
103 }
104});
105
106rl.on("close", () => {
107 log("read loop closed; exiting");
108 process.exit(0);
109});
110
111/**
112 * @param {Frame & {name?: string, args?: string}} frame
113 */
114function handleCommand(frame) {
115 const name = String(frame.name ?? "");
116 const args = String(frame.args ?? "").trim();
117 const id = String(frame.id ?? "");
118
119 switch (name) {
120 case "now": {
121 const now = new Date();
122 const human = now.toLocaleString(undefined, {
123 weekday: "short",
124 year: "numeric",
125 month: "short",
126 day: "numeric",
127 hour: "2-digit",
128 minute: "2-digit",
129 second: "2-digit",
130 });
131 const iso = now.toISOString();
132 send({
133 type: "command_response",
134 id,
135 action: "display",
136 display: `local: ${human}\niso : ${iso}`,
137 });
138 return;
139 }
140
141 case "uptime": {
142 const ms = Date.now() - STARTED_AT;
143 const seconds = Math.round(ms / 1000);
144 const focus = args ? `Focus on the topic: ${args}.` : "";
145 send({
146 type: "command_response",
147 id,
148 action: "prompt",
149 prompt:
150 `The clock extension has been running for ${seconds}s in this zot session. ` +
151 `Riff on that briefly in one short sentence — be a little dramatic. ${focus}`.trim(),
152 });
153 return;
154 }
155
156 default:
157 send({
158 type: "command_response",
159 id,
160 action: "noop",
161 error: `clock: unknown command /${name}`,
162 });
163 }
164}