v0.2.93 e9bc0c6

index.js RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close