sfora-cli 0.12.0 → 0.12.1
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/dist/api-client.d.ts +12 -0
- package/dist/api-client.js +11 -0
- package/dist/chat.d.ts +16 -0
- package/dist/chat.js +32 -0
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli-args.js +4 -0
- package/dist/cli.js +23 -1
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -576,6 +576,18 @@ export declare class SforaApiClient {
|
|
|
576
576
|
sendRoomMessage(roomId: string, body: string): Promise<{
|
|
577
577
|
messageId: string;
|
|
578
578
|
}>;
|
|
579
|
+
/**
|
|
580
|
+
* `POST /api/presence` with `{ roomId?, client? }` — one presence beat.
|
|
581
|
+
*
|
|
582
|
+
* `client` is a short slug ("cli", "claude-code") that names what kind of
|
|
583
|
+
* client is on the line; the app reads it out next to the online dot. The
|
|
584
|
+
* server sanitizes and owns the field on every beat, so a beat without a
|
|
585
|
+
* label also clears a stale one.
|
|
586
|
+
*/
|
|
587
|
+
heartbeat(params?: {
|
|
588
|
+
roomId?: string;
|
|
589
|
+
client?: string;
|
|
590
|
+
}): Promise<void>;
|
|
579
591
|
/** `GET /v1/fs/inbox/mentions.md` — markdown summary of unread mentions. */
|
|
580
592
|
readInbox(): Promise<string>;
|
|
581
593
|
/** `GET /v1/fs/me/api-key` — text identity (no key material). */
|
package/dist/api-client.js
CHANGED
|
@@ -637,6 +637,17 @@ export class SforaApiClient {
|
|
|
637
637
|
const res = await this.#request("POST", `/api/rooms/${encodeURIComponent(roomId)}/messages`, JSON.stringify({ body }), undefined, "application/json");
|
|
638
638
|
return this.#jsonFrom(res);
|
|
639
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* `POST /api/presence` with `{ roomId?, client? }` — one presence beat.
|
|
642
|
+
*
|
|
643
|
+
* `client` is a short slug ("cli", "claude-code") that names what kind of
|
|
644
|
+
* client is on the line; the app reads it out next to the online dot. The
|
|
645
|
+
* server sanitizes and owns the field on every beat, so a beat without a
|
|
646
|
+
* label also clears a stale one.
|
|
647
|
+
*/
|
|
648
|
+
async heartbeat(params = {}) {
|
|
649
|
+
await this.#request("POST", "/api/presence", JSON.stringify(params), undefined, "application/json");
|
|
650
|
+
}
|
|
640
651
|
/** `GET /v1/fs/inbox/mentions.md` — markdown summary of unread mentions. */
|
|
641
652
|
async readInbox() {
|
|
642
653
|
return this.#text("/v1/fs/inbox/mentions.md");
|
package/dist/chat.d.ts
CHANGED
|
@@ -17,6 +17,22 @@
|
|
|
17
17
|
* print twice.
|
|
18
18
|
*/
|
|
19
19
|
import type { AgentEventsPage, ChatMessage, Room } from "./api-client.js";
|
|
20
|
+
/**
|
|
21
|
+
* A presence client label as the server stores it: lowercase, `[a-z0-9-]`
|
|
22
|
+
* only, at most 32 chars. Anything that sanitizes to nothing is nothing —
|
|
23
|
+
* the caller falls through to its next guess.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sanitizeClientSlug(raw: string | undefined): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* What kind of client is on the line, for the presence label.
|
|
28
|
+
*
|
|
29
|
+
* Order: an explicit `--client` flag wins; then `AI_AGENT` (the part before
|
|
30
|
+
* the first underscore — `claude-code_2-1-233_agent` names "claude-code");
|
|
31
|
+
* then `CLAUDE_CODE_ENTRYPOINT` being set at all means Claude Code is
|
|
32
|
+
* driving; else this is a plain terminal — "cli". Env is passed in, not
|
|
33
|
+
* read, so every rung is testable.
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectClient(env: Record<string, string | undefined>, flag?: string): string;
|
|
20
36
|
/** How `sfora rooms` and `sfora join` spell a room name as an argument. */
|
|
21
37
|
export declare function roomSlug(name: string): string;
|
|
22
38
|
export type RoomMatch = {
|
package/dist/chat.js
CHANGED
|
@@ -19,6 +19,38 @@
|
|
|
19
19
|
import { colors } from "./render.js";
|
|
20
20
|
import { MAX_BACKOFF_MS } from "./watch.js";
|
|
21
21
|
const dim = (text) => `${colors.dim}${text}${colors.reset}`;
|
|
22
|
+
// ─── Client identity (presence label) ────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* A presence client label as the server stores it: lowercase, `[a-z0-9-]`
|
|
25
|
+
* only, at most 32 chars. Anything that sanitizes to nothing is nothing —
|
|
26
|
+
* the caller falls through to its next guess.
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeClientSlug(raw) {
|
|
29
|
+
if (!raw)
|
|
30
|
+
return undefined;
|
|
31
|
+
const slug = raw.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 32);
|
|
32
|
+
return slug || undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What kind of client is on the line, for the presence label.
|
|
36
|
+
*
|
|
37
|
+
* Order: an explicit `--client` flag wins; then `AI_AGENT` (the part before
|
|
38
|
+
* the first underscore — `claude-code_2-1-233_agent` names "claude-code");
|
|
39
|
+
* then `CLAUDE_CODE_ENTRYPOINT` being set at all means Claude Code is
|
|
40
|
+
* driving; else this is a plain terminal — "cli". Env is passed in, not
|
|
41
|
+
* read, so every rung is testable.
|
|
42
|
+
*/
|
|
43
|
+
export function detectClient(env, flag) {
|
|
44
|
+
const fromFlag = sanitizeClientSlug(flag);
|
|
45
|
+
if (fromFlag)
|
|
46
|
+
return fromFlag;
|
|
47
|
+
const fromAgent = sanitizeClientSlug(env.AI_AGENT?.split("_")[0]);
|
|
48
|
+
if (fromAgent)
|
|
49
|
+
return fromAgent;
|
|
50
|
+
if (env.CLAUDE_CODE_ENTRYPOINT !== undefined)
|
|
51
|
+
return "claude-code";
|
|
52
|
+
return "cli";
|
|
53
|
+
}
|
|
22
54
|
// ─── Room resolution ─────────────────────────────────────────────────
|
|
23
55
|
/** How `sfora rooms` and `sfora join` spell a room name as an argument. */
|
|
24
56
|
export function roomSlug(name) {
|
package/dist/cli-args.d.ts
CHANGED
package/dist/cli-args.js
CHANGED
|
@@ -77,6 +77,10 @@ export function parseArgs(argv) {
|
|
|
77
77
|
args.limit = Number.parseInt(argv[++i] ?? "", 10);
|
|
78
78
|
else if (a.startsWith("--limit="))
|
|
79
79
|
args.limit = Number.parseInt(a.slice("--limit=".length), 10);
|
|
80
|
+
else if (a === "--client")
|
|
81
|
+
args.client = argv[++i];
|
|
82
|
+
else if (a.startsWith("--client="))
|
|
83
|
+
args.client = a.slice("--client=".length);
|
|
80
84
|
else if (!a.startsWith("-")) {
|
|
81
85
|
if (!args.command)
|
|
82
86
|
args.command = a;
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { LocalWorkspace, initWorkspace, findWorkspace, migrateWorkspaceStages, }
|
|
|
20
20
|
import { runMcpServer } from "./mcp-server.js";
|
|
21
21
|
import { readConfig, writeConfig, resolveSettings, upsertProfile, effectiveProfiles, DEFAULT_URL, } from "./config.js";
|
|
22
22
|
import { parseArgs } from "./cli-args.js";
|
|
23
|
-
import { chatTailLoop, renderChatMessage, renderRoomList, resolveRoomRef, roomSlug, } from "./chat.js";
|
|
23
|
+
import { chatTailLoop, detectClient, renderChatMessage, renderRoomList, resolveRoomRef, roomSlug, } from "./chat.js";
|
|
24
24
|
const HELP = `sfora — the CLI for your sfora workspace
|
|
25
25
|
|
|
26
26
|
Get started (no account needed):
|
|
@@ -69,6 +69,10 @@ Chat:
|
|
|
69
69
|
sfora chat <room> -m "text" Send one message and exit (for scripts
|
|
70
70
|
and agents)
|
|
71
71
|
|
|
72
|
+
Chat shows others what's on the line: the CLI reports itself in presence,
|
|
73
|
+
and a coding agent is named automatically (Claude Code sets its own
|
|
74
|
+
environment). Add --client <name> to say it yourself.
|
|
75
|
+
|
|
72
76
|
Write, and watch others write:
|
|
73
77
|
sfora blocks <path> List a document's addressable blocks
|
|
74
78
|
sfora put <path> <file.md> Write a file (add --block <id> for one block)
|
|
@@ -801,11 +805,19 @@ async function runChat(args, client) {
|
|
|
801
805
|
await client.joinRoom(room._id);
|
|
802
806
|
console.log(`${colors.green}✓${colors.reset} Joined ${tag}`);
|
|
803
807
|
}
|
|
808
|
+
// Presence is decoration: a beat that fails changes nothing about the
|
|
809
|
+
// conversation, so every beat here is best-effort and silent. The label
|
|
810
|
+
// says WHAT is on the line — "cli", or "claude-code" when a coding agent
|
|
811
|
+
// is driving — so the app can show the terminal next to the online dot.
|
|
812
|
+
const clientLabel = detectClient(process.env, args.client);
|
|
813
|
+
const beat = () => client.heartbeat({ roomId: room._id, client: clientLabel }).catch(() => { });
|
|
804
814
|
// One-shot send.
|
|
805
815
|
if (args.message !== undefined) {
|
|
806
816
|
const text = args.message.trim();
|
|
807
817
|
if (!text)
|
|
808
818
|
throw new Error('usage: sfora chat <room> -m "text"');
|
|
819
|
+
// One beat alongside the send — a script that speaks was here, briefly.
|
|
820
|
+
void beat();
|
|
809
821
|
const { messageId } = await client.sendRoomMessage(room._id, text);
|
|
810
822
|
if (args.json) {
|
|
811
823
|
console.log(JSON.stringify({ messageId, roomId: room._id }, null, 2));
|
|
@@ -831,6 +843,12 @@ async function runChat(args, client) {
|
|
|
831
843
|
for (const msg of messages)
|
|
832
844
|
console.log(renderChatMessage(msg, Date.now()));
|
|
833
845
|
process.stderr.write(`${colors.dim}${tag} — type to send · /quit (or ^C) to leave${colors.reset}\n`);
|
|
846
|
+
// Sitting in the room is being present in it: beat now, then every ~45s
|
|
847
|
+
// (presence expires server-side, so the cadence just has to outrun the
|
|
848
|
+
// timeout). `unref` so the timer never keeps the process alive on its own.
|
|
849
|
+
void beat();
|
|
850
|
+
const pulse = setInterval(() => void beat(), 45_000);
|
|
851
|
+
pulse.unref?.();
|
|
834
852
|
// The prompt and the tail share one screen: an arriving message clears the
|
|
835
853
|
// prompt line, prints, and readline redraws the prompt with whatever was
|
|
836
854
|
// being typed. `readline.clearLine` over anything fancier — robust beats
|
|
@@ -904,6 +922,10 @@ async function runChat(args, client) {
|
|
|
904
922
|
stop = true;
|
|
905
923
|
inFlight.abort();
|
|
906
924
|
rl.close();
|
|
925
|
+
clearInterval(pulse);
|
|
926
|
+
// A parting beat, best-effort — awaited so quitting doesn't race process
|
|
927
|
+
// exit, but never allowed to hold the door.
|
|
928
|
+
await beat();
|
|
907
929
|
await tail.catch(() => { });
|
|
908
930
|
if (isTty)
|
|
909
931
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfora-cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Your sfora workspace as a markdown filesystem — a CLI + MCP server. Post/task/doc, ls/cat/grep, and a shell so agents operate sfora natively.",
|
|
6
6
|
"keywords": [
|