wheems 0.0.1 → 0.0.3
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/CHANGELOG.md +68 -0
- package/README.md +44 -10
- package/dist/cli/commands/service.js +4 -5
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/main.js +7 -6
- package/dist/cli/program.d.ts +3 -1
- package/dist/cli/program.js +55 -5
- package/dist/cli/welcome.d.ts +68 -0
- package/dist/cli/welcome.js +163 -0
- package/dist/contracts/channel.d.ts +15 -1
- package/dist/doctor/doctor.d.ts +2 -1
- package/dist/doctor/doctor.js +73 -21
- package/dist/drivers/channel/slack/connect.d.ts +5 -0
- package/dist/drivers/channel/slack/connect.js +53 -3
- package/dist/drivers/channel/slack/slack-channel.d.ts +1 -0
- package/dist/drivers/channel/slack/slack-channel.js +23 -2
- package/dist/drivers/engine/claude-code.d.ts +2 -0
- package/dist/drivers/engine/claude-code.js +43 -1
- package/dist/drivers/runtime/herdr/hc.js +1 -1
- package/dist/drivers/runtime/herdr/herdr-runtime.js +1 -1
- package/dist/drivers/vector-store/sqlite.js +2 -1
- package/dist/knowledge/search.d.ts +5 -0
- package/dist/knowledge/search.js +3 -1
- package/dist/secrets.d.ts +1 -1
- package/dist/service/conversations.js +24 -5
- package/dist/service/launchd.js +2 -2
- package/dist/service/service.js +8 -3
- package/dist/testing/memory-channel.d.ts +3 -0
- package/dist/testing/memory-channel.js +1 -0
- package/dist/util/node-support.d.ts +11 -0
- package/dist/util/node-support.js +21 -0
- package/dist/util/sqlite.d.ts +12 -0
- package/dist/util/sqlite.js +35 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,74 @@
|
|
|
3
3
|
Wheems follows [semantic versioning](https://semver.org). While it's 0.0.x, any release may
|
|
4
4
|
change anything, including commands and config formats, without a migration path.
|
|
5
5
|
|
|
6
|
+
## 0.0.3
|
|
7
|
+
|
|
8
|
+
A new look for the cloud in the terminal, and clearer help when a Slack token is wrong. Still an
|
|
9
|
+
early preview: not ready for real use. The first release published from GitHub.
|
|
10
|
+
|
|
11
|
+
**Changed**
|
|
12
|
+
|
|
13
|
+
- The cloud on the welcome screen and in `wheems setup` is now the mascot's outline, with its eye
|
|
14
|
+
and smile, instead of a solid shape whose face got lost. It's drawn in the terminal's text colour
|
|
15
|
+
(white on a dark terminal, near-black on a light one) rather than blue. It's left out when the
|
|
16
|
+
output isn't a terminal (piped or saved to a file) and when the terminal is narrower than 53
|
|
17
|
+
columns, so only the name, tagline and status are shown.
|
|
18
|
+
- The cloud winks once when it appears, in an interactive terminal. Set `WHEEMS_NO_ANIMATION` to
|
|
19
|
+
stop it; it never winks in CI, in a dumb terminal, or when the output isn't a terminal.
|
|
20
|
+
|
|
21
|
+
**Fixed**
|
|
22
|
+
|
|
23
|
+
- When a pasted Slack token isn't right, setup says why instead of only "The bot token starts with
|
|
24
|
+
xoxb-": the app-level token pasted for the bot token (or the other way round), a user or refresh
|
|
25
|
+
token, two pastes run together, something in front of the token, or a space in it, each with
|
|
26
|
+
where in Slack to find the right one. It names tokens only by their standard prefix.
|
|
27
|
+
|
|
28
|
+
## 0.0.2
|
|
29
|
+
|
|
30
|
+
Fixes from the first preview, a welcome screen, and checks that catch common set-up problems.
|
|
31
|
+
Still an early preview: not ready for real use.
|
|
32
|
+
|
|
33
|
+
**Added**
|
|
34
|
+
|
|
35
|
+
- `wheems` on its own shows a welcome screen: the cloud from the brand kit, in blue, beside the
|
|
36
|
+
name and "Your crew, ready for work.", then how the install is doing (workspaces and agents,
|
|
37
|
+
the service, search, and anything waiting for review). `wheems setup` opens with the cloud too.
|
|
38
|
+
- `wheems init` creates the install when there isn't one, so setup can be scripted from a fresh
|
|
39
|
+
machine (connecting Slack still needs `wheems setup`).
|
|
40
|
+
- `wheems doctor` checks that Claude Code is signed in (`claude auth status`), and that the app is
|
|
41
|
+
a member of each workspace's chat channel: Slack lists public channels the app isn't in, and
|
|
42
|
+
sends no messages from them. The service warns about such a channel when it starts.
|
|
43
|
+
|
|
44
|
+
**Fixed**
|
|
45
|
+
|
|
46
|
+
- Search needs SQLite full-text search (FTS5), which Node only bundles from 22.16 in the 22
|
|
47
|
+
line and from 24; no Node 23 release has it. `engines` is now `^22.16.0 || >=24`, the CLI and
|
|
48
|
+
`wheems service install` refuse other versions with a plain message, `wheems doctor` checks
|
|
49
|
+
that this Node's SQLite has full-text search, and search says so instead of failing with
|
|
50
|
+
"no such module: fts5".
|
|
51
|
+
- `npm run build` now marks `dist/cli/main.js` executable, so a `wheems` linked with `npm link`
|
|
52
|
+
runs instead of failing with "Permission denied".
|
|
53
|
+
- Slack messages with a file attached were silently ignored. Now a file with no words gets a reply
|
|
54
|
+
asking for the text, and with words the agent gets them and a note that files can't be passed
|
|
55
|
+
on yet.
|
|
56
|
+
- After a quick reply from an agent, Wheems could add a false "The agent finished without
|
|
57
|
+
replying here": it recorded when a message was sent only after typing it in.
|
|
58
|
+
- After connecting Slack, setup says to restart the service (which reads tokens only when it
|
|
59
|
+
starts) and how to let someone else talk to the agents.
|
|
60
|
+
- On Linux, `wheems doctor` warns that the service needs macOS for now instead of passing.
|
|
61
|
+
- Docs: where to install Herdr core ([herdr.dev/core](https://herdr.dev/core/)), npm as the way
|
|
62
|
+
to install, inviting the app to a channel, session names, which commands take `--home`, a
|
|
63
|
+
broken table in config.md, and milestone numbers in code comments.
|
|
64
|
+
|
|
65
|
+
**Known issues**
|
|
66
|
+
|
|
67
|
+
- Only one Slack connection, named `slack`, can be set up from `wheems setup` and `wheems init`;
|
|
68
|
+
a second Slack app has to be added by hand.
|
|
69
|
+
- The Slack app's manifest asks for interactivity, which nothing uses until approvals with
|
|
70
|
+
buttons (milestone 3).
|
|
71
|
+
- The launchd label is fixed, so only one install per Mac user can run as a service.
|
|
72
|
+
- `wheems kb search --json` reports inbox notes as `kind: note`; the path tells them apart.
|
|
73
|
+
|
|
6
74
|
## 0.0.1
|
|
7
75
|
|
|
8
76
|
The first preview: milestones 0 to 2 of the [roadmap](docs/roadmap.md). Not ready for real use.
|
package/README.md
CHANGED
|
@@ -78,9 +78,11 @@ Wheems was formerly called Opinr.
|
|
|
78
78
|
|
|
79
79
|
You need:
|
|
80
80
|
|
|
81
|
-
- macOS (the service runs under launchd; Linux comes
|
|
82
|
-
|
|
83
|
-
-
|
|
81
|
+
- macOS (the service runs under launchd; Linux, with systemd, comes with the always-on
|
|
82
|
+
milestone)
|
|
83
|
+
- Node.js 22.16 or later in the 22 line, or 24 or later (not 23: its SQLite has no full-text
|
|
84
|
+
search, which Wheems needs)
|
|
85
|
+
- Herdr core 0.1.20 or later (`hc`): see [herdr.dev/core](https://herdr.dev/core/) to install it
|
|
84
86
|
- A Slack workspace where you can add apps
|
|
85
87
|
|
|
86
88
|
Install the `wheems` command from npm:
|
|
@@ -102,9 +104,10 @@ npm link # puts the wheems command on your PATH
|
|
|
102
104
|
Clone it anywhere except `~/wheems`. That folder is your install: your private config, notes
|
|
103
105
|
and knowledge base, kept in a git folder of its own that is never pushed anywhere.
|
|
104
106
|
|
|
105
|
-
If your default `node`
|
|
107
|
+
If your default `node` isn't one of those (Homebrew's `node` may be 23), use one that is, such as Homebrew's
|
|
106
108
|
(`brew install node@22`), and run `/opt/homebrew/opt/node@22/bin/node dist/cli/main.js` in place
|
|
107
|
-
of `wheems`. `wheems doctor` says which Node.js it runs on and fails when it's
|
|
109
|
+
of `wheems`. `wheems doctor` says which Node.js it runs on, and fails when it's unsupported or its SQLite has no
|
|
110
|
+
full-text search.
|
|
108
111
|
|
|
109
112
|
## Set up
|
|
110
113
|
|
|
@@ -141,6 +144,14 @@ One Slack app serves every workspace, so you do this once.
|
|
|
141
144
|
5. Give your Slack member ID (your profile, **⋯**, **Copy member ID**). Only people on this list
|
|
142
145
|
can talk to your agents. Messages from anyone else are ignored.
|
|
143
146
|
|
|
147
|
+
### Inviting the app to a channel
|
|
148
|
+
|
|
149
|
+
A workspace that chats in a channel needs the app in that channel: in the channel, type
|
|
150
|
+
`/invite` and choose the app. Slack lists public channels the app isn't in, so a channel name
|
|
151
|
+
that's spelt right can still hear nothing; the service warns at start-up and `wheems doctor`
|
|
152
|
+
checks it. The service looks channels up when it starts, so after creating a channel or renaming
|
|
153
|
+
one, run `wheems service restart`.
|
|
154
|
+
|
|
144
155
|
### Trusting the workspace folder
|
|
145
156
|
|
|
146
157
|
Claude Code and Codex ask once whether to trust a new folder, and nobody can answer that from
|
|
@@ -149,7 +160,9 @@ folder, then press Ctrl+C twice to come back. `wheems doctor` checks the trust.
|
|
|
149
160
|
|
|
150
161
|
### Without the menu
|
|
151
162
|
|
|
152
|
-
Each part has a command of its own, for scripts
|
|
163
|
+
Each part has a command of its own, for scripts. `wheems init` creates the install too when
|
|
164
|
+
there isn't one, so a fresh machine can start there; connecting Slack still needs `wheems setup`,
|
|
165
|
+
because you type its tokens in yourself:
|
|
153
166
|
|
|
154
167
|
```bash
|
|
155
168
|
wheems init personal # a workspace on Claude Code
|
|
@@ -226,8 +239,9 @@ can be given skills, but nothing starts them yet: teams of agents come in later
|
|
|
226
239
|
Message the Slack app (or the workspace's channel). Each message you send starts a
|
|
227
240
|
**conversation**, and replies in its thread carry it on.
|
|
228
241
|
|
|
229
|
-
- Each conversation is its own engine session in Herdr, named after the workspace
|
|
230
|
-
Watch it or take over with
|
|
242
|
+
- Each conversation is its own engine session in Herdr, named after the workspace with a short
|
|
243
|
+
id for the thread, such as `personal-3f9a2c1b`. Watch it or take over with
|
|
244
|
+
`hc attach <session>`.
|
|
231
245
|
- A 👀 reaction means your message has reached the agent. It goes when the agent answers.
|
|
232
246
|
- The agent's last message of each turn is posted to the thread as its answer. The answer is
|
|
233
247
|
read from the engine's own record of the conversation, never from the screen.
|
|
@@ -506,13 +520,15 @@ from its workspace's skills. Pinned skill versions must exist in the library.
|
|
|
506
520
|
|
|
507
521
|
## Command reference
|
|
508
522
|
|
|
509
|
-
|
|
510
|
-
`~/wheems`).
|
|
523
|
+
Commands that use the install take `--home <dir>` for the install folder (default:
|
|
524
|
+
`$WHEEMS_HOME`, then `~/wheems`). `wheems service restart`, `service uninstall` and
|
|
525
|
+
`slack manifest` don't need it.
|
|
511
526
|
|
|
512
527
|
**For you**
|
|
513
528
|
|
|
514
529
|
| Command | What it does |
|
|
515
530
|
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
|
|
531
|
+
| `wheems` | The welcome screen: the cloud, and how the install is doing (workspaces, the service, search, review) |
|
|
516
532
|
| `wheems setup` | The setup menu: engines, Slack, workspaces, knowledge, the service, doctor |
|
|
517
533
|
| `wheems init <workspace> [--name] [--channel] [--engine] [--model]` | Creates a workspace with its chief of staff. `--engine` is `claude-code` (default), `codex` or `opencode` |
|
|
518
534
|
| `wheems doctor [--quick] [--json]` | Checks the machine, Herdr core and the install |
|
|
@@ -649,6 +665,7 @@ skills:
|
|
|
649
665
|
| `WHEEMS_SECRETS` | The secrets file (default `~/.config/wheems/secrets.json`, or under `$XDG_CONFIG_HOME`) |
|
|
650
666
|
| `WHEEMS_WORKSPACE` | The workspace `wheems kb search` searches. Wheems sets it in every agent session |
|
|
651
667
|
| `WHEEMS_CONVERSATION` | The conversation an agent's commands belong to. Wheems sets it in every agent session |
|
|
668
|
+
| `WHEEMS_NO_ANIMATION` | Set to anything to stop the cloud winking on the welcome screen and in `wheems setup` |
|
|
652
669
|
|
|
653
670
|
### Config the schemas accept but nothing uses yet
|
|
654
671
|
|
|
@@ -782,6 +799,23 @@ also run it against a fake `hc`, which is what CI uses. Set `WHEEMS_TEST_OLLAMA_
|
|
|
782
799
|
| `examples/install/` | A complete example install |
|
|
783
800
|
| `docs/` | Design documents |
|
|
784
801
|
|
|
802
|
+
### Releasing
|
|
803
|
+
|
|
804
|
+
GitHub publishes each release. To release, merge a pull request to `main` that:
|
|
805
|
+
|
|
806
|
+
1. changes `version` in `package.json` (and `package-lock.json`);
|
|
807
|
+
2. adds a `## <version>` section to `CHANGELOG.md`.
|
|
808
|
+
|
|
809
|
+
The [release workflow](.github/workflows/release.yml) then runs the full check on Linux and
|
|
810
|
+
macOS and publishes to npm. It then tags the commit `v<version>` and creates a GitHub release
|
|
811
|
+
with that changelog section as its notes. A push to `main` whose version is already published and
|
|
812
|
+
tagged does nothing.
|
|
813
|
+
|
|
814
|
+
npm trusts the workflow through
|
|
815
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm token is stored
|
|
816
|
+
anywhere. The package's settings on npmjs.com name this repository and `release.yml` as its
|
|
817
|
+
trusted publisher.
|
|
818
|
+
|
|
785
819
|
## More documentation
|
|
786
820
|
|
|
787
821
|
The documents in [`docs/`](docs/) go deeper, and some describe the design of milestones not built
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { nodeVersionProblem } from '../../util/node-support.js';
|
|
5
5
|
import { defaultHome, paths } from '../../home.js';
|
|
6
6
|
import { request } from '../../service/control.js';
|
|
7
7
|
import { installService, plistPath, restartService, serviceStatus, uninstallService, } from '../../service/launchd.js';
|
|
8
8
|
import { runService } from '../../service/service.js';
|
|
9
|
-
import { isAtLeast } from '../../util/version.js';
|
|
10
9
|
import { packageVersion } from '../version.js';
|
|
11
10
|
/** The script that is running this CLI, resolved through npm links. */
|
|
12
11
|
export function cliPath() {
|
|
@@ -14,9 +13,9 @@ export function cliPath() {
|
|
|
14
13
|
}
|
|
15
14
|
/** Installs the service under launchd with this Node.js and build, and waits for it to answer. */
|
|
16
15
|
export async function installAndWait(where) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const problem = nodeVersionProblem(process.versions.node);
|
|
17
|
+
if (problem !== null)
|
|
18
|
+
throw new Error(problem);
|
|
20
19
|
const cli = cliPath();
|
|
21
20
|
if (!cli.endsWith('.js')) {
|
|
22
21
|
throw new Error('Install the service from a build of Wheems (npm run build), not from source.');
|
|
@@ -156,6 +156,10 @@ async function channels(options, where) {
|
|
|
156
156
|
doc.setIn(['connections', connection, 'allowed_users'], doc.createNode(result.allowedUsers, { flow: true }));
|
|
157
157
|
await writeFile(where.install, doc.toString());
|
|
158
158
|
ui.success(`${entry.label} is connected. Its tokens are saved privately in ${where.secrets}`);
|
|
159
|
+
ui.info([
|
|
160
|
+
'The service reads tokens when it starts: restart it (Service) for these to take effect.',
|
|
161
|
+
`To let someone else talk to your agents, add their member ID to allowed_users under connections.${connection} in install.yaml, then restart the service.`,
|
|
162
|
+
].join('\n'));
|
|
159
163
|
return true;
|
|
160
164
|
}
|
|
161
165
|
/**
|
package/dist/cli/main.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Check the Node.js version before loading anything that needs a newer one, so an
|
|
3
|
-
// Node.js gets a plain message rather than a stack trace from a dependency.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
// Check the Node.js version before loading anything that needs a newer one, so an
|
|
3
|
+
// unsupported Node.js gets a plain message rather than a stack trace from a dependency.
|
|
4
|
+
import { nodeVersionProblem } from '../util/node-support.js';
|
|
5
|
+
const problem = nodeVersionProblem(process.versions.node);
|
|
6
|
+
if (problem !== null) {
|
|
7
|
+
process.stderr.write(`${problem}\nThis is ${process.execPath}.\n`);
|
|
7
8
|
process.exit(1);
|
|
8
9
|
}
|
|
9
10
|
const { defaultIo, run } = await import('./program.js');
|
|
@@ -13,7 +14,7 @@ process.exitCode = await run(process.argv.slice(2), {
|
|
|
13
14
|
err: (text) => process.stderr.write(text),
|
|
14
15
|
env: process.env,
|
|
15
16
|
color,
|
|
17
|
+
columns: process.stdout.isTTY ? process.stdout.columns : undefined,
|
|
16
18
|
...defaultIo(),
|
|
17
19
|
});
|
|
18
|
-
export {};
|
|
19
20
|
//# sourceMappingURL=main.js.map
|
package/dist/cli/program.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface Io {
|
|
|
8
8
|
err: (text: string) => void;
|
|
9
9
|
env: NodeJS.ProcessEnv;
|
|
10
10
|
color: boolean;
|
|
11
|
+
/** The terminal's width, or undefined when the output isn't a terminal. */
|
|
12
|
+
columns?: number | undefined;
|
|
11
13
|
registry: () => DriverRegistry;
|
|
12
14
|
stdin: () => Promise<string>;
|
|
13
15
|
}
|
|
@@ -15,6 +17,6 @@ export interface Io {
|
|
|
15
17
|
export declare function defaultIo(): Pick<Io, 'registry' | 'stdin'>;
|
|
16
18
|
/** Builds the `wheems` command. */
|
|
17
19
|
export declare function createProgram(io: Io): Command;
|
|
18
|
-
/** Runs the CLI and returns the exit code. */
|
|
20
|
+
/** Runs the CLI and returns the exit code. With no command, it shows the welcome screen. */
|
|
19
21
|
export declare function run(argv: string[], io: Io): Promise<number>;
|
|
20
22
|
export declare function formatReport(report: DoctorReport, color: boolean): string;
|
package/dist/cli/program.js
CHANGED
|
@@ -15,8 +15,9 @@ import { registerSkill } from './commands/skill.js';
|
|
|
15
15
|
import { registerNote } from './commands/note.js';
|
|
16
16
|
import { registerReply } from './commands/reply.js';
|
|
17
17
|
import { registerService } from './commands/service.js';
|
|
18
|
-
import { setup } from './commands/setup.js';
|
|
18
|
+
import { ensureInstall, setup } from './commands/setup.js';
|
|
19
19
|
import { clackUi } from './clack-ui.js';
|
|
20
|
+
import { canWink, renderWelcomeHead, renderWelcomeTail, SHOW_CURSOR, showCloud, welcomeStatus, wink, } from './welcome.js';
|
|
20
21
|
import { installAndWait, serviceAnswers } from './commands/service.js';
|
|
21
22
|
import { runCommand, runInteractive } from '../util/process.js';
|
|
22
23
|
import { SetupCancelled } from '../contracts/setup.js';
|
|
@@ -88,7 +89,7 @@ export function createProgram(io) {
|
|
|
88
89
|
});
|
|
89
90
|
program
|
|
90
91
|
.command('setup')
|
|
91
|
-
.description('Set up Wheems:
|
|
92
|
+
.description('Set up Wheems: engines, chat channels, workspaces, search and the service')
|
|
92
93
|
.option('--home <dir>', 'the install folder (default: $WHEEMS_HOME or ~/wheems)')
|
|
93
94
|
.action(async (options) => {
|
|
94
95
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -98,6 +99,13 @@ export function createProgram(io) {
|
|
|
98
99
|
const where = paths(home, io.env);
|
|
99
100
|
const registry = io.registry();
|
|
100
101
|
const ui = clackUi();
|
|
102
|
+
io.out(renderWelcomeHead({
|
|
103
|
+
version: packageVersion(),
|
|
104
|
+
color: io.color,
|
|
105
|
+
cloud: showCloud(io.columns),
|
|
106
|
+
}));
|
|
107
|
+
if (canWink(io.columns, io.env))
|
|
108
|
+
await winkOnce(io);
|
|
101
109
|
ui.intro(' Wheems setup ');
|
|
102
110
|
try {
|
|
103
111
|
await setup({
|
|
@@ -133,7 +141,7 @@ export function createProgram(io) {
|
|
|
133
141
|
});
|
|
134
142
|
program
|
|
135
143
|
.command('init')
|
|
136
|
-
.description('Create a workspace with its chief of staff')
|
|
144
|
+
.description('Create a workspace with its chief of staff (and the install, if there is none)')
|
|
137
145
|
.argument('<workspace>', 'the workspace id, such as personal')
|
|
138
146
|
.option('--name <name>', 'its display name (default: from the id)')
|
|
139
147
|
.option('--channel <channel>', "chat in this channel instead of the bot's direct messages")
|
|
@@ -154,8 +162,13 @@ export function createProgram(io) {
|
|
|
154
162
|
throw new Error(`No engine "${options.engine}". Choose one of: ${known}`);
|
|
155
163
|
}
|
|
156
164
|
const setup = entry.engine;
|
|
165
|
+
const home = resolve(options.home ?? defaultHome(io.env));
|
|
166
|
+
// On a fresh machine, init makes the install too, so setup can be scripted.
|
|
167
|
+
const createdInstall = await ensureInstall(paths(home, io.env));
|
|
168
|
+
if (createdInstall)
|
|
169
|
+
io.out(`Created the install at ${home} (a git folder)\n`);
|
|
157
170
|
const dir = await initWorkspace({
|
|
158
|
-
home
|
|
171
|
+
home,
|
|
159
172
|
id,
|
|
160
173
|
env: io.env,
|
|
161
174
|
engine: {
|
|
@@ -206,8 +219,45 @@ export function createProgram(io) {
|
|
|
206
219
|
function shellPath(path) {
|
|
207
220
|
return /^[\w./-]+$/.test(path) ? path : `'${path.replaceAll("'", `'\\''`)}'`;
|
|
208
221
|
}
|
|
209
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* The cloud winks once. Ctrl+C meanwhile still gives the terminal its cursor back before
|
|
224
|
+
* stopping, as it would have done without the wink.
|
|
225
|
+
*/
|
|
226
|
+
async function winkOnce(io) {
|
|
227
|
+
const interrupted = () => {
|
|
228
|
+
io.out(SHOW_CURSOR);
|
|
229
|
+
process.exit(130);
|
|
230
|
+
};
|
|
231
|
+
process.once('SIGINT', interrupted);
|
|
232
|
+
try {
|
|
233
|
+
await wink({ version: packageVersion(), color: io.color, write: io.out });
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
process.off('SIGINT', interrupted);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Runs the CLI and returns the exit code. With no command, it shows the welcome screen. */
|
|
210
240
|
export async function run(argv, io) {
|
|
241
|
+
if (argv.length === 0) {
|
|
242
|
+
const home = resolve(defaultHome(io.env));
|
|
243
|
+
// The welcome never fails: a problem reading the install becomes one of its lines.
|
|
244
|
+
// The status is read while the cloud is drawn and winks.
|
|
245
|
+
const reading = welcomeStatus(home, io.env).catch((error) => [
|
|
246
|
+
{
|
|
247
|
+
mark: '!',
|
|
248
|
+
text: `Couldn't read the install at ${home}: ${error instanceof Error ? error.message : String(error)}. Run wheems doctor.`,
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
io.out(renderWelcomeHead({
|
|
252
|
+
version: packageVersion(),
|
|
253
|
+
color: io.color,
|
|
254
|
+
cloud: showCloud(io.columns),
|
|
255
|
+
}));
|
|
256
|
+
if (canWink(io.columns, io.env))
|
|
257
|
+
await winkOnce(io);
|
|
258
|
+
io.out(renderWelcomeTail({ status: await reading, color: io.color }));
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
211
261
|
const program = createProgram(io);
|
|
212
262
|
try {
|
|
213
263
|
await program.parseAsync(argv, { from: 'user' });
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cloud: the mascot's outline (the brand kit's mascot.svg), traced in Unicode half blocks,
|
|
3
|
+
* with its eye and smile placed by hand so they read at this size. Half blocks are in every
|
|
4
|
+
* monospace font and most terminals draw them as exact rectangles, so it looks the same
|
|
5
|
+
* everywhere; an outline also reads on light and dark backgrounds alike. It has no colour of
|
|
6
|
+
* its own: it's drawn in the terminal's text colour, like the brand's ink app icon, so it's white
|
|
7
|
+
* on a dark terminal and near-black on a light one.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CLOUD: string[];
|
|
10
|
+
/** The brand line under the name. */
|
|
11
|
+
export declare const TAGLINE = "Your crew, ready for work.";
|
|
12
|
+
/** The columns the cloud and the words beside it need. */
|
|
13
|
+
export declare const WELCOME_WIDTH: number;
|
|
14
|
+
/**
|
|
15
|
+
* Whether to draw the cloud: only in a terminal wide enough for it and the words beside it.
|
|
16
|
+
* Piped into a file or another program, or read out by a screen reader, it would only be
|
|
17
|
+
* noise. `columns` is undefined when the output isn't a terminal.
|
|
18
|
+
*/
|
|
19
|
+
export declare function showCloud(columns: number | undefined): boolean;
|
|
20
|
+
export interface WelcomeStatus {
|
|
21
|
+
mark: '✓' | '!' | '-';
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
interface WelcomeOptions {
|
|
25
|
+
version: string;
|
|
26
|
+
status: WelcomeStatus[];
|
|
27
|
+
color: boolean;
|
|
28
|
+
/** Ends with the commands to try next. On by default. */
|
|
29
|
+
hints?: boolean;
|
|
30
|
+
/** Draws the cloud. On by default; see showCloud for when to leave it out. */
|
|
31
|
+
cloud?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** The welcome screen: the cloud with the name and tagline beside it, then the install's status. */
|
|
34
|
+
export declare function renderWelcome(options: WelcomeOptions): string;
|
|
35
|
+
/** The top of the welcome screen: the cloud and the words beside it, or just the words. */
|
|
36
|
+
export declare function renderWelcomeHead(options: Omit<WelcomeOptions, 'status'>): string;
|
|
37
|
+
/** The rest of the welcome screen: the install's status and the commands to try. */
|
|
38
|
+
export declare function renderWelcomeTail(options: Omit<WelcomeOptions, 'version' | 'cloud'>): string;
|
|
39
|
+
/**
|
|
40
|
+
* Whether the cloud may wink: only when it's drawn, so in a wide enough terminal, and not in
|
|
41
|
+
* CI, a dumb terminal, or for someone who has set WHEEMS_NO_ANIMATION.
|
|
42
|
+
*/
|
|
43
|
+
export declare function canWink(columns: number | undefined, env: NodeJS.ProcessEnv): boolean;
|
|
44
|
+
/** How long the eye stays open before the wink, and how long it stays shut. */
|
|
45
|
+
export declare const WINK_TIMING: {
|
|
46
|
+
readonly before: 400;
|
|
47
|
+
readonly shut: 150;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The cloud winks once: run straight after renderWelcomeHead is written, with the cursor on the
|
|
51
|
+
* line under the cloud. It redraws only the eye's row, hides the cursor meanwhile and always
|
|
52
|
+
* shows it again.
|
|
53
|
+
*/
|
|
54
|
+
export declare function wink(options: {
|
|
55
|
+
version: string;
|
|
56
|
+
color: boolean;
|
|
57
|
+
write: (text: string) => void;
|
|
58
|
+
sleep?: (ms: number) => Promise<void>;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
export declare const HIDE_CURSOR = "\u001B[?25l";
|
|
61
|
+
export declare const SHOW_CURSOR = "\u001B[?25h";
|
|
62
|
+
/**
|
|
63
|
+
* What the welcome screen says about this install: its workspaces and agents, the service,
|
|
64
|
+
* search, and anything waiting for review. Quick: it reads files and pings the service, and
|
|
65
|
+
* never starts a session.
|
|
66
|
+
*/
|
|
67
|
+
export declare function welcomeStatus(home: string, env: NodeJS.ProcessEnv, ping?: (socket: string) => Promise<boolean>): Promise<WelcomeStatus[]>;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadInstall } from '../config/load.js';
|
|
4
|
+
import { paths } from '../home.js';
|
|
5
|
+
import { listAboutYou } from '../review/review.js';
|
|
6
|
+
import { request } from '../service/control.js';
|
|
7
|
+
import { listProposals } from '../skills/proposals.js';
|
|
8
|
+
/**
|
|
9
|
+
* The cloud: the mascot's outline (the brand kit's mascot.svg), traced in Unicode half blocks,
|
|
10
|
+
* with its eye and smile placed by hand so they read at this size. Half blocks are in every
|
|
11
|
+
* monospace font and most terminals draw them as exact rectangles, so it looks the same
|
|
12
|
+
* everywhere; an outline also reads on light and dark backgrounds alike. It has no colour of
|
|
13
|
+
* its own: it's drawn in the terminal's text colour, like the brand's ink app icon, so it's white
|
|
14
|
+
* on a dark terminal and near-black on a light one.
|
|
15
|
+
*/
|
|
16
|
+
// Every character is one UTF-16 unit, so .length is the width in columns.
|
|
17
|
+
export const CLOUD = [
|
|
18
|
+
' ▄█████▄▄',
|
|
19
|
+
' ▄█▀ ▀▀█▄',
|
|
20
|
+
' ▄█████▀ ▀█▄',
|
|
21
|
+
' ██ ██',
|
|
22
|
+
' ▄██ █▀',
|
|
23
|
+
' ██▀ ██ ▀█▄▄',
|
|
24
|
+
'██ ▀█▄',
|
|
25
|
+
'██ ▀▄▄▀ ██',
|
|
26
|
+
' ██▄ ▄█▀',
|
|
27
|
+
' ▀▀█████████████████▀',
|
|
28
|
+
];
|
|
29
|
+
/** The brand line under the name. */
|
|
30
|
+
export const TAGLINE = 'Your crew, ready for work.';
|
|
31
|
+
const CLOUD_WIDTH = Math.max(...CLOUD.map((line) => line.length));
|
|
32
|
+
/** The columns the cloud and the words beside it need. */
|
|
33
|
+
export const WELCOME_WIDTH = CLOUD_WIDTH + 3 + TAGLINE.length;
|
|
34
|
+
/**
|
|
35
|
+
* Whether to draw the cloud: only in a terminal wide enough for it and the words beside it.
|
|
36
|
+
* Piped into a file or another program, or read out by a screen reader, it would only be
|
|
37
|
+
* noise. `columns` is undefined when the output isn't a terminal.
|
|
38
|
+
*/
|
|
39
|
+
export function showCloud(columns) {
|
|
40
|
+
return columns !== undefined && columns >= WELCOME_WIDTH;
|
|
41
|
+
}
|
|
42
|
+
const BOLD = '\x1b[1m';
|
|
43
|
+
const DIM = '\x1b[2m';
|
|
44
|
+
const RESET = '\x1b[0m';
|
|
45
|
+
/** The welcome screen: the cloud with the name and tagline beside it, then the install's status. */
|
|
46
|
+
export function renderWelcome(options) {
|
|
47
|
+
return renderWelcomeHead(options) + renderWelcomeTail(options);
|
|
48
|
+
}
|
|
49
|
+
/** The row of the cloud with the eye in it, and the eye's columns. */
|
|
50
|
+
const EYE = { row: 5, from: 6, to: 8 };
|
|
51
|
+
function cloudRow(options, row, line) {
|
|
52
|
+
const paint = (code, text) => (options.color ? `${code}${text}${RESET}` : text);
|
|
53
|
+
const beside = {
|
|
54
|
+
4: paint(BOLD, 'wheems') + ' ' + paint(DIM, options.version),
|
|
55
|
+
5: TAGLINE,
|
|
56
|
+
};
|
|
57
|
+
const text = beside[row];
|
|
58
|
+
return text === undefined ? line : `${line.padEnd(CLOUD_WIDTH)} ${text}`;
|
|
59
|
+
}
|
|
60
|
+
/** The top of the welcome screen: the cloud and the words beside it, or just the words. */
|
|
61
|
+
export function renderWelcomeHead(options) {
|
|
62
|
+
const paint = (code, text) => (options.color ? `${code}${text}${RESET}` : text);
|
|
63
|
+
const lines = options.cloud === false
|
|
64
|
+
? [paint(BOLD, 'wheems') + ' ' + paint(DIM, options.version), TAGLINE]
|
|
65
|
+
: CLOUD.map((line, row) => cloudRow(options, row, line));
|
|
66
|
+
return ['', ...lines].join('\n') + '\n';
|
|
67
|
+
}
|
|
68
|
+
/** The rest of the welcome screen: the install's status and the commands to try. */
|
|
69
|
+
export function renderWelcomeTail(options) {
|
|
70
|
+
const paint = (code, text) => (options.color ? `${code}${text}${RESET}` : text);
|
|
71
|
+
const marks = { '✓': '\x1b[32m', '!': '\x1b[33m', '-': DIM };
|
|
72
|
+
return [
|
|
73
|
+
'',
|
|
74
|
+
...options.status.map((s) => `${paint(marks[s.mark], s.mark)} ${s.text}`),
|
|
75
|
+
...(options.status.length > 0 ? [''] : []),
|
|
76
|
+
...(options.hints === false
|
|
77
|
+
? []
|
|
78
|
+
: [paint(DIM, 'wheems setup · wheems doctor · wheems --help'), '']),
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether the cloud may wink: only when it's drawn, so in a wide enough terminal, and not in
|
|
83
|
+
* CI, a dumb terminal, or for someone who has set WHEEMS_NO_ANIMATION.
|
|
84
|
+
*/
|
|
85
|
+
export function canWink(columns, env) {
|
|
86
|
+
return (showCloud(columns) &&
|
|
87
|
+
env.TERM !== 'dumb' &&
|
|
88
|
+
(env.CI ?? '') === '' &&
|
|
89
|
+
(env.WHEEMS_NO_ANIMATION ?? '') === '');
|
|
90
|
+
}
|
|
91
|
+
/** How long the eye stays open before the wink, and how long it stays shut. */
|
|
92
|
+
export const WINK_TIMING = { before: 400, shut: 150 };
|
|
93
|
+
/**
|
|
94
|
+
* The cloud winks once: run straight after renderWelcomeHead is written, with the cursor on the
|
|
95
|
+
* line under the cloud. It redraws only the eye's row, hides the cursor meanwhile and always
|
|
96
|
+
* shows it again.
|
|
97
|
+
*/
|
|
98
|
+
export async function wink(options) {
|
|
99
|
+
const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
100
|
+
const up = CLOUD.length - EYE.row;
|
|
101
|
+
const open = CLOUD[EYE.row] ?? '';
|
|
102
|
+
const shut = open.slice(0, EYE.from) + '▄▄' + open.slice(EYE.to);
|
|
103
|
+
// Up to the eye's row, clear it, draw it again, and back down to where the cursor was.
|
|
104
|
+
const redraw = (line) => {
|
|
105
|
+
options.write(`\x1b[${String(up)}A\r\x1b[2K${cloudRow(options, EYE.row, line)}\x1b[${String(up)}B\r`);
|
|
106
|
+
};
|
|
107
|
+
options.write(HIDE_CURSOR);
|
|
108
|
+
try {
|
|
109
|
+
await sleep(WINK_TIMING.before);
|
|
110
|
+
redraw(shut);
|
|
111
|
+
await sleep(WINK_TIMING.shut);
|
|
112
|
+
redraw(open);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
options.write(SHOW_CURSOR);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export const HIDE_CURSOR = '\x1b[?25l';
|
|
119
|
+
export const SHOW_CURSOR = '\x1b[?25h';
|
|
120
|
+
/**
|
|
121
|
+
* What the welcome screen says about this install: its workspaces and agents, the service,
|
|
122
|
+
* search, and anything waiting for review. Quick: it reads files and pings the service, and
|
|
123
|
+
* never starts a session.
|
|
124
|
+
*/
|
|
125
|
+
export async function welcomeStatus(home, env, ping = (socket) => request(socket, { op: 'ping' }, 1000).then((answer) => answer.ok, () => false)) {
|
|
126
|
+
const where = paths(home, env);
|
|
127
|
+
const hasInstall = await readFile(join(home, 'install.yaml')).then(() => true, () => false);
|
|
128
|
+
if (!hasInstall) {
|
|
129
|
+
return [{ mark: '!', text: `No install at ${tilde(home, env)} yet: run wheems setup` }];
|
|
130
|
+
}
|
|
131
|
+
const install = await loadInstall(home);
|
|
132
|
+
const workspaces = install.workspaces.filter((w) => w.config);
|
|
133
|
+
const agents = workspaces.reduce((n, w) => n + w.agents.length, 0);
|
|
134
|
+
const status = [];
|
|
135
|
+
status.push(workspaces.length > 0
|
|
136
|
+
? {
|
|
137
|
+
mark: '✓',
|
|
138
|
+
text: `${count(workspaces.length, 'workspace')}, ${count(agents, 'agent')} in ${tilde(home, env)}`,
|
|
139
|
+
}
|
|
140
|
+
: { mark: '!', text: 'No workspaces yet: run wheems setup' });
|
|
141
|
+
status.push((await ping(where.socket))
|
|
142
|
+
? { mark: '✓', text: 'The service is running' }
|
|
143
|
+
: { mark: '!', text: "The service isn't running: wheems service install" });
|
|
144
|
+
const embeddings = install.config?.knowledge.embeddings;
|
|
145
|
+
status.push(embeddings
|
|
146
|
+
? { mark: '✓', text: `Search by meaning with ${embeddings.model ?? embeddings.driver}` }
|
|
147
|
+
: { mark: '-', text: 'Search by keyword only (wheems setup → Knowledge for meaning)' });
|
|
148
|
+
const waiting = (await listProposals(where.state)).length + (await listAboutYou(where.state)).length;
|
|
149
|
+
if (waiting > 0) {
|
|
150
|
+
status.push({ mark: '!', text: `${count(waiting, 'thing')} waiting for you: wheems review` });
|
|
151
|
+
}
|
|
152
|
+
return status;
|
|
153
|
+
}
|
|
154
|
+
function count(n, noun) {
|
|
155
|
+
return `${String(n)} ${noun}${n === 1 ? '' : 's'}`;
|
|
156
|
+
}
|
|
157
|
+
function tilde(path, env) {
|
|
158
|
+
const home = env.HOME;
|
|
159
|
+
return home !== undefined && home !== '' && path.startsWith(`${home}/`)
|
|
160
|
+
? `~${path.slice(home.length)}`
|
|
161
|
+
: path;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=welcome.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Driver, HealthCheck } from './driver.js';
|
|
2
2
|
/**
|
|
3
3
|
* The channel contract: sends and receives messages, buttons and threads.
|
|
4
|
-
* Version one ships Slack (milestone 1) and Telegram and email (milestone
|
|
4
|
+
* Version one ships Slack (milestone 1) and Telegram and email (milestone 10).
|
|
5
5
|
*
|
|
6
6
|
* A channel driver holds one connection, such as one Slack app. Channels inside
|
|
7
7
|
* it are addressed by the driver's own channel id.
|
|
@@ -27,6 +27,16 @@ export interface ChannelDriver extends Driver<'channel'> {
|
|
|
27
27
|
react(message: MessageRef, reaction: string, on: boolean): Promise<void>;
|
|
28
28
|
/** Disconnects. No handler calls happen after it resolves. Safe to call twice. */
|
|
29
29
|
stop(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Optional: a channel's id from its name (or its id), or null when the app can't see it.
|
|
32
|
+
* Drivers without it take channel names as ids.
|
|
33
|
+
*/
|
|
34
|
+
resolveChannel?(nameOrId: string): Promise<string | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Optional: whether the app is a member of a channel, so messages there reach it. Null when it
|
|
37
|
+
* can't tell. Seeing a channel isn't enough: Slack lists public channels the app isn't in.
|
|
38
|
+
*/
|
|
39
|
+
isMember?(channelId: string): Promise<boolean | null>;
|
|
30
40
|
}
|
|
31
41
|
export interface MessageRef {
|
|
32
42
|
channel: string;
|
|
@@ -56,6 +66,10 @@ export interface InboundMessage {
|
|
|
56
66
|
/** True for a direct message between the person and the bot. */
|
|
57
67
|
direct: boolean;
|
|
58
68
|
receivedAt: Date;
|
|
69
|
+
/** Files attached to it, by name. Wheems can't pass their contents on yet. */
|
|
70
|
+
files?: {
|
|
71
|
+
name: string;
|
|
72
|
+
}[];
|
|
59
73
|
}
|
|
60
74
|
export interface ButtonPress {
|
|
61
75
|
type: 'button';
|