v0.4.2 83c2c60

index.js RawHistoryFolder

1// clock — a zut 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//   zut ext install /path/to/this/dir
19//
20// Then in zut:
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 zut. 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 zut to $ZUT_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  protocol_version: 2,
56  name: NAME,
57  version: VERSION,
58  capabilities: ["commands"],
59});
60
61// 2. Register every command we can handle.
62send({
63  type: "register_command",
64  name: "now",
65  description: "show the current local time (no model call)",
66});
67send({
68  type: "register_command",
69  name: "uptime",
70  description: "ask the agent to riff on how long the clock ext has run",
71});
72
73// 3. Read frames until stdin closes (zut shuts us down).
74const rl = createInterface({ input: stdin, crlfDelay: Infinity });
75
76rl.on("line", (line) => {
77  /** @type {Frame} */
78  let frame;
79  try {
80    frame = JSON.parse(line);
81  } catch (err) {
82    log(`malformed frame: ${err}`);
83    return;
84  }
85
86  switch (frame.type) {
87    case "hello_ack":
88      if (frame.protocol_version !== 2) {
89        log(`unsupported protocol version: ${frame.protocol_version}`);
90        process.exitCode = 1;
91        rl.close();
92        break;
93      }
94      log(
95        `connected to zut ${frame.zut_version} (${frame.provider}/${frame.model})`,
96      );
97      break;
98
99    case "command_invoked":
100      handleCommand(frame);
101      break;
102
103    case "shutdown":
104      send({ type: "shutdown_ack" });
105      rl.close();
106      break;
107
108    default:
109      log(`unknown frame type: ${frame.type}`);
110  }
111});
112
113rl.on("close", () => {
114  log("read loop closed; exiting");
115  process.exit(0);
116});
117
118/**
119 * @param {Frame & {name?: string, args?: string}} frame
120 */
121function handleCommand(frame) {
122  const name = String(frame.name ?? "");
123  const args = String(frame.args ?? "").trim();
124  const id = String(frame.id ?? "");
125
126  switch (name) {
127    case "now": {
128      const now = new Date();
129      const human = now.toLocaleString(undefined, {
130        weekday: "short",
131        year: "numeric",
132        month: "short",
133        day: "numeric",
134        hour: "2-digit",
135        minute: "2-digit",
136        second: "2-digit",
137      });
138      const iso = now.toISOString();
139      send({
140        type: "command_response",
141        id,
142        action: "display",
143        display: `local: ${human}\niso  : ${iso}`,
144      });
145      return;
146    }
147
148    case "uptime": {
149      const ms = Date.now() - STARTED_AT;
150      const seconds = Math.round(ms / 1000);
151      const focus = args ? `Focus on the topic: ${args}.` : "";
152      send({
153        type: "command_response",
154        id,
155        action: "prompt",
156        prompt:
157          `The clock extension has been running for ${seconds}s in this zut session. ` +
158          `Riff on that briefly in one short sentence — be a little dramatic. ${focus}`.trim(),
159      });
160      return;
161    }
162
163    default:
164      send({
165        type: "command_response",
166        id,
167        action: "noop",
168        error: `clock: unknown command /${name}`,
169      });
170  }
171}

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