sfora-cli 0.16.0 → 0.17.0
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/README.md +18 -0
- package/dist/agent-webhook.d.ts +20 -0
- package/dist/agent-webhook.js +42 -0
- package/dist/api-client.d.ts +97 -0
- package/dist/api-client.js +68 -0
- package/dist/ask.d.ts +51 -0
- package/dist/ask.js +70 -0
- package/dist/attachments-node.d.ts +7 -0
- package/dist/attachments-node.js +15 -0
- package/dist/attachments.d.ts +112 -0
- package/dist/attachments.js +254 -0
- package/dist/block-commands.d.ts +10 -0
- package/dist/block-commands.js +28 -0
- package/dist/chat.d.ts +15 -0
- package/dist/chat.js +7 -0
- package/dist/cli-args.d.ts +7 -0
- package/dist/cli-args.js +28 -1
- package/dist/cli.d.ts +12 -1
- package/dist/cli.js +186 -19
- package/dist/format/linkUrls.d.ts +2 -0
- package/dist/format/linkUrls.js +48 -0
- package/dist/format/postMarkdown.d.ts +12 -1
- package/dist/format/postMarkdown.js +9 -2
- package/dist/index.d.ts +23 -1
- package/dist/index.js +17 -1
- package/dist/local-core/skills.d.ts +25 -0
- package/dist/local-core/skills.js +93 -0
- package/dist/mcp-description.d.ts +11 -0
- package/dist/mcp-description.js +29 -0
- package/dist/mcp-server.d.ts +5 -1
- package/dist/mcp-server.js +28 -18
- package/dist/shell-commands.d.ts +7 -1
- package/dist/shell-commands.js +49 -3
- package/dist/skills-command.d.ts +1 -1
- package/dist/skills-command.js +20 -1
- package/dist/skills-packet/sfora-asks/SKILL.md +49 -0
- package/dist/skills-packet/sfora-asks/references/asks.md +41 -0
- package/dist/skills-packet/sfora-board/SKILL.md +53 -0
- package/dist/skills-packet/sfora-board/references/board.md +60 -0
- package/dist/skills-packet/sfora-board/references/plan.md +25 -0
- package/dist/skills-packet/sfora-chat/SKILL.md +55 -0
- package/dist/skills-packet/sfora-chat/references/rooms.md +45 -0
- package/dist/skills-packet/sfora-chat/references/waiting.md +37 -0
- package/dist/skills-packet/sfora-live-edit/SKILL.md +62 -0
- package/dist/skills-packet/sfora-live-edit/references/collisions.md +54 -0
- package/dist/skills-packet/sfora-live-edit/references/http.md +63 -0
- package/dist/skills-packet/sfora-live-edit/references/live-editing.md +49 -0
- package/dist/skills-packet/sfora-setup/SKILL.md +37 -0
- package/dist/skills-packet/sfora-setup/references/sign-in.md +43 -0
- package/dist/skills-packet/sfora-skills/SKILL.md +54 -0
- package/dist/skills-packet/sfora-skills/references/skills.md +78 -0
- package/dist/skills-packet/sfora-troubleshoot/SKILL.md +39 -0
- package/dist/skills-packet/sfora-troubleshoot/references/sharp-edges.md +80 -0
- package/dist/skills-packet/sfora-write/SKILL.md +53 -0
- package/dist/skills-packet/sfora-write/references/attachments.md +15 -0
- package/dist/skills-packet/sfora-write/references/blocks.md +36 -0
- package/dist/skills-packet/sfora-write/references/posts-and-docs.md +56 -0
- package/dist/skills-packet.d.ts +63 -0
- package/dist/skills-packet.js +166 -0
- package/dist/typing.d.ts +23 -0
- package/dist/typing.js +62 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watch.d.ts +78 -1
- package/dist/watch.js +109 -0
- package/package.json +3 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `attachments <post>` — the files a person attached to a post (card #822).
|
|
3
|
+
*
|
|
4
|
+
* Three doors, one module: the `sfora attachments` verb (list, or write the
|
|
5
|
+
* files with `--out`), the shell's `attachments` command (list only — the
|
|
6
|
+
* shell's fs is the workspace, not the disk), and the MCP tools' list and
|
|
7
|
+
* fetch, where an image comes back as image content a vision client can see.
|
|
8
|
+
*
|
|
9
|
+
* Every byte is fetched through the API with the caller's key; the server
|
|
10
|
+
* applies the post's own read rule, so nothing here decides access. What this
|
|
11
|
+
* module does decide is what lands on disk: a name that cannot leave the
|
|
12
|
+
* `--out` directory, and a file whose type and size match what was listed.
|
|
13
|
+
*
|
|
14
|
+
* No `node:` imports, so the hosted MCP route can import it; the CLI hands in
|
|
15
|
+
* the disk through {@link AttachmentIo} (see attachments-node.ts).
|
|
16
|
+
*/
|
|
17
|
+
import { SforaApiError } from "./api-client.js";
|
|
18
|
+
import { colors } from "./render.js";
|
|
19
|
+
/**
|
|
20
|
+
* The server's limit (Convex's HTTP response cap, checked against the stored
|
|
21
|
+
* size). Mirrored here so a listed file over it is skipped without a request.
|
|
22
|
+
*/
|
|
23
|
+
export const ATTACHMENT_MAX_BYTES = 20 * 1024 * 1024;
|
|
24
|
+
const ok = (stdout, stderr = "") => ({ stdout, stderr, exitCode: 0 });
|
|
25
|
+
const fail = (stderr, stdout = "") => ({
|
|
26
|
+
stdout,
|
|
27
|
+
stderr: stderr.endsWith("\n") ? stderr : `${stderr}\n`,
|
|
28
|
+
exitCode: 1,
|
|
29
|
+
});
|
|
30
|
+
function apiMessage(error) {
|
|
31
|
+
if (error instanceof SforaApiError) {
|
|
32
|
+
return error.status === 0 ? `could not reach the server — ${error.message}` : error.message;
|
|
33
|
+
}
|
|
34
|
+
return error instanceof Error ? error.message : String(error);
|
|
35
|
+
}
|
|
36
|
+
function humanSize(bytes) {
|
|
37
|
+
if (bytes < 1024)
|
|
38
|
+
return `${bytes} B`;
|
|
39
|
+
if (bytes < 1024 * 1024)
|
|
40
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
41
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A file name that is safe to write inside one directory: the last path
|
|
45
|
+
* segment only (either separator), no control characters, no leading dots,
|
|
46
|
+
* and unique among the names already `taken` (`shot.png`, `shot-2.png`, …).
|
|
47
|
+
* An empty result falls back to `attachment-<id>`.
|
|
48
|
+
*/
|
|
49
|
+
export function safeAttachmentName(name, id, taken) {
|
|
50
|
+
const base = (name.split(/[\\/]/).pop() ?? "")
|
|
51
|
+
.replace(/[\u0000-\u001f\u007f]/g, "")
|
|
52
|
+
.trim()
|
|
53
|
+
.replace(/^\.+/, "")
|
|
54
|
+
.slice(0, 200);
|
|
55
|
+
const chosen = base || `attachment-${id}`;
|
|
56
|
+
const dot = chosen.lastIndexOf(".");
|
|
57
|
+
const [stem, ext] = dot > 0 ? [chosen.slice(0, dot), chosen.slice(dot)] : [chosen, ""];
|
|
58
|
+
let candidate = chosen;
|
|
59
|
+
for (let n = 2; taken.has(candidate.toLowerCase()); n++)
|
|
60
|
+
candidate = `${stem}-${n}${ext}`;
|
|
61
|
+
taken.add(candidate.toLowerCase());
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
/** The media type without parameters, lowercased: `image/png; x=y` → `image/png`. */
|
|
65
|
+
const essence = (type) => type.split(";")[0].trim().toLowerCase();
|
|
66
|
+
/**
|
|
67
|
+
* Whether a download is what the listing promised. The server serves a type
|
|
68
|
+
* it does not trust as `application/octet-stream`, so that is accepted for
|
|
69
|
+
* any listed type; anything else must match exactly, as must the byte count.
|
|
70
|
+
*/
|
|
71
|
+
function mismatch(row, got) {
|
|
72
|
+
const served = essence(got.type);
|
|
73
|
+
if (served !== essence(row.type) && served !== "application/octet-stream") {
|
|
74
|
+
return `served as ${served || "no type"}, listed as ${row.type}`;
|
|
75
|
+
}
|
|
76
|
+
if (got.bytes.byteLength !== row.size) {
|
|
77
|
+
return `${got.bytes.byteLength} bytes, listed as ${row.size}`;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* `sfora attachments <post> [--out <dir>] [--json]`.
|
|
83
|
+
*
|
|
84
|
+
* Without `--out` it lists. With it, it writes each file into the directory
|
|
85
|
+
* and prints one local path per line (so an agent can open the images); a
|
|
86
|
+
* file that is too large, or does not match its listing, is skipped with a
|
|
87
|
+
* line on stderr and the command exits 1 after writing the rest.
|
|
88
|
+
*/
|
|
89
|
+
export async function attachmentsCommand(client, fsPath, options = {}) {
|
|
90
|
+
let rows;
|
|
91
|
+
try {
|
|
92
|
+
rows = await client.listAttachments(fsPath);
|
|
93
|
+
client.takeResponseInfo();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return fail(`${colors.red}error:${colors.reset} ${apiMessage(error)}`);
|
|
97
|
+
}
|
|
98
|
+
if (options.out === undefined) {
|
|
99
|
+
if (options.json)
|
|
100
|
+
return ok(`${JSON.stringify(rows, null, 2)}\n`);
|
|
101
|
+
if (rows.length === 0)
|
|
102
|
+
return ok(`${colors.dim}(no attachments)${colors.reset}\n`);
|
|
103
|
+
const width = Math.max(...rows.map((r) => r.name.length));
|
|
104
|
+
const lines = rows.map((r) => `${r.name.padEnd(width)} ${colors.dim}${r.type} · ${humanSize(r.size)} · ${r.id}${colors.reset}`);
|
|
105
|
+
return ok(`${lines.join("\n")}\n`);
|
|
106
|
+
}
|
|
107
|
+
const io = options.io;
|
|
108
|
+
if (!io)
|
|
109
|
+
return fail("attachments --out needs a local disk (use the sfora CLI)");
|
|
110
|
+
await io.mkdir(options.out);
|
|
111
|
+
const taken = new Set();
|
|
112
|
+
const written = [];
|
|
113
|
+
const problems = [];
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
if (row.size > ATTACHMENT_MAX_BYTES) {
|
|
116
|
+
problems.push(`${row.name}: too large to download (${humanSize(row.size)}; the limit is ${humanSize(ATTACHMENT_MAX_BYTES)})`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
let got;
|
|
120
|
+
try {
|
|
121
|
+
got = await client.downloadAttachment(fsPath, row.id);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
problems.push(`${row.name}: ${apiMessage(error)}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const wrong = mismatch(row, got);
|
|
128
|
+
if (wrong) {
|
|
129
|
+
problems.push(`${row.name}: not written — ${wrong}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const path = io.join(options.out, safeAttachmentName(row.name, row.id, taken));
|
|
133
|
+
await io.writeFile(path, got.bytes);
|
|
134
|
+
written.push({ ...row, path });
|
|
135
|
+
}
|
|
136
|
+
const stdout = options.json
|
|
137
|
+
? `${JSON.stringify(written, null, 2)}\n`
|
|
138
|
+
: written.map((w) => `${w.path}\n`).join("");
|
|
139
|
+
if (problems.length) {
|
|
140
|
+
return fail(problems.map((p) => `${colors.red}error:${colors.reset} ${p}`).join("\n"), stdout);
|
|
141
|
+
}
|
|
142
|
+
return ok(stdout);
|
|
143
|
+
}
|
|
144
|
+
/** The two tools both MCP servers list, so their schemas cannot drift. */
|
|
145
|
+
export const ATTACHMENT_TOOLS = [
|
|
146
|
+
{
|
|
147
|
+
name: "attachments",
|
|
148
|
+
description: "List the files attached to a post (screenshots, documents). Pass the post's path, e.g. /projects/web/posts/2026-10-06-errors.md. Returns JSON rows { id, name, type, size }; fetch one with the `attachment` tool.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
path: { type: "string", description: "The post's absolute path under /projects/<slug>/posts/ (or drafts/)." },
|
|
153
|
+
},
|
|
154
|
+
required: ["path"],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "attachment",
|
|
159
|
+
description: "Fetch one attachment of a post by id (from the `attachments` tool). Images come back as image content you can look at; text files as text; other files as a description only.",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
path: { type: "string", description: "The post's absolute path." },
|
|
164
|
+
id: { type: "string", description: "The attachment id from the `attachments` list." },
|
|
165
|
+
},
|
|
166
|
+
required: ["path", "id"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
export const isAttachmentTool = (name) => name === "attachments" || name === "attachment";
|
|
171
|
+
/**
|
|
172
|
+
* The largest image the MCP fetch returns as image content: 3 MB. Base64
|
|
173
|
+
* makes it ~4 MB of JSON, which fits the hosted route's 4.5 MB response
|
|
174
|
+
* limit (a Vercel function), and it is under the 5 MB a Claude image may
|
|
175
|
+
* be. Larger images are metadata plus a pointer to `attachments --out`.
|
|
176
|
+
*/
|
|
177
|
+
export const MCP_IMAGE_MAX_BYTES = 3 * 1024 * 1024;
|
|
178
|
+
/** How much of a text file the MCP fetch inlines: 256 KB, decoded as UTF-8; the rest is cut with a note. */
|
|
179
|
+
export const MCP_TEXT_MAX_BYTES = 256 * 1024;
|
|
180
|
+
// Exactly these are inlined as text. Everything else — SVG and HTML
|
|
181
|
+
// included, which are markup a client might render — is metadata only.
|
|
182
|
+
const TEXT_TYPES = new Set(["text/plain", "text/markdown", "text/csv", "application/json"]);
|
|
183
|
+
// The image types vision clients accept.
|
|
184
|
+
const IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
185
|
+
function base64(bytes) {
|
|
186
|
+
let binary = "";
|
|
187
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
188
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
189
|
+
}
|
|
190
|
+
return btoa(binary);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Answer an MCP call to `attachments` (no id) or `attachment` (with one).
|
|
194
|
+
* Errors are results with `isError`, never throws: the servers return them
|
|
195
|
+
* as tool output the agent can read and act on.
|
|
196
|
+
*/
|
|
197
|
+
export async function attachmentToolResult(client, args) {
|
|
198
|
+
const error = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
199
|
+
const path = typeof args.path === "string" ? args.path.trim() : "";
|
|
200
|
+
if (!path.startsWith("/"))
|
|
201
|
+
return error("Pass the post's absolute path, e.g. /projects/web/posts/<file>.md.");
|
|
202
|
+
try {
|
|
203
|
+
const rows = await client.listAttachments(path);
|
|
204
|
+
if (args.id === undefined) {
|
|
205
|
+
return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }], isError: false };
|
|
206
|
+
}
|
|
207
|
+
const row = rows.find((r) => r.id === args.id);
|
|
208
|
+
if (!row)
|
|
209
|
+
return error(`No attachment ${String(args.id)} on ${path}. Its attachments: ${rows.map((r) => r.id).join(", ") || "none"}.`);
|
|
210
|
+
const label = `${row.name} (${row.type}, ${humanSize(row.size)})`;
|
|
211
|
+
const type = essence(row.type);
|
|
212
|
+
const metadataOnly = (why) => ({
|
|
213
|
+
content: [{ type: "text", text: `${label}: ${why}. Download it with: sfora attachments ${path} --out <dir>` }],
|
|
214
|
+
isError: false,
|
|
215
|
+
});
|
|
216
|
+
// An image is all or nothing; a text file is cut at MCP_TEXT_MAX_BYTES,
|
|
217
|
+
// so it may be fetched up to the API's own cap.
|
|
218
|
+
const cap = IMAGE_TYPES.has(type) ? MCP_IMAGE_MAX_BYTES : TEXT_TYPES.has(type) ? ATTACHMENT_MAX_BYTES : 0;
|
|
219
|
+
if (cap === 0)
|
|
220
|
+
return metadataOnly("not an image or a plain-text file, so it is not shown here");
|
|
221
|
+
// The listed size first, so an oversized file is never fetched; the
|
|
222
|
+
// download is then held to that size exactly (`mismatch`).
|
|
223
|
+
if (row.size > cap) {
|
|
224
|
+
return metadataOnly(`larger than the ${humanSize(cap)} this tool returns`);
|
|
225
|
+
}
|
|
226
|
+
const got = await client.downloadAttachment(path, row.id);
|
|
227
|
+
const wrong = mismatch(row, got);
|
|
228
|
+
if (wrong)
|
|
229
|
+
return error(`${label}: refused — ${wrong}.`);
|
|
230
|
+
if (IMAGE_TYPES.has(type)) {
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{ type: "text", text: label },
|
|
234
|
+
{ type: "image", data: base64(got.bytes), mimeType: type },
|
|
235
|
+
],
|
|
236
|
+
isError: false,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const cut = got.bytes.byteLength > MCP_TEXT_MAX_BYTES;
|
|
240
|
+
// `stream: true` holds back a UTF-8 sequence the cut split, rather than
|
|
241
|
+
// ending the text on a replacement character.
|
|
242
|
+
const text = new TextDecoder().decode(got.bytes.subarray(0, MCP_TEXT_MAX_BYTES), { stream: cut });
|
|
243
|
+
const note = cut
|
|
244
|
+
? `\n\n[truncated: the first ${humanSize(MCP_TEXT_MAX_BYTES)} of ${humanSize(got.bytes.byteLength)}. The whole file: sfora attachments ${path} --out <dir>]`
|
|
245
|
+
: "";
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: `${label}\n\n${text}${note}` }],
|
|
248
|
+
isError: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return error(apiMessage(e));
|
|
253
|
+
}
|
|
254
|
+
}
|
package/dist/block-commands.d.ts
CHANGED
|
@@ -83,3 +83,13 @@ export declare function putCommand(client: SforaApiClient, fsPath: string, body:
|
|
|
83
83
|
export declare function urlCommand(client: SforaApiClient, fsPath: string, options?: {
|
|
84
84
|
json?: boolean;
|
|
85
85
|
}): Promise<CommandOutput>;
|
|
86
|
+
/**
|
|
87
|
+
* `backlinks <path>` — what links to a doc, post or task (card #682).
|
|
88
|
+
*
|
|
89
|
+
* The same rows as the app's "Linked from", scoped to what this member may
|
|
90
|
+
* read, each with the token to paste. Plain text is one line per source;
|
|
91
|
+
* `--json` is the door's answer as served.
|
|
92
|
+
*/
|
|
93
|
+
export declare function backlinksCommand(client: SforaApiClient, fsPath: string, options?: {
|
|
94
|
+
json?: boolean;
|
|
95
|
+
}): Promise<CommandOutput>;
|
package/dist/block-commands.js
CHANGED
|
@@ -154,3 +154,31 @@ export async function urlCommand(client, fsPath, options = {}) {
|
|
|
154
154
|
return fail(`${colors.red}error:${colors.reset} ${apiMessage(error)}`);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* `backlinks <path>` — what links to a doc, post or task (card #682).
|
|
159
|
+
*
|
|
160
|
+
* The same rows as the app's "Linked from", scoped to what this member may
|
|
161
|
+
* read, each with the token to paste. Plain text is one line per source;
|
|
162
|
+
* `--json` is the door's answer as served.
|
|
163
|
+
*/
|
|
164
|
+
export async function backlinksCommand(client, fsPath, options = {}) {
|
|
165
|
+
try {
|
|
166
|
+
const view = await client.readBacklinks(fsPath);
|
|
167
|
+
client.takeResponseInfo();
|
|
168
|
+
if (options.json)
|
|
169
|
+
return ok(`${JSON.stringify(view, null, 2)}\n`);
|
|
170
|
+
const n = view.backlinks.length;
|
|
171
|
+
const head = `${view.target.title} — linked from ${n} ${n === 1 ? "place" : "places"}${view.truncated ? " (and more)" : ""}`;
|
|
172
|
+
const lines = view.backlinks.map((row) => {
|
|
173
|
+
const where = row.anchors.length
|
|
174
|
+
? ` ${colors.dim}${row.anchors.map((a) => `#${a}`).join(" ")}${colors.reset}`
|
|
175
|
+
: "";
|
|
176
|
+
const token = row.token ? ` ${colors.dim}${row.token}${colors.reset}` : "";
|
|
177
|
+
return ` ${row.kind.padEnd(7)} ${row.title}${where}${token}`;
|
|
178
|
+
});
|
|
179
|
+
return ok(`${[head, ...lines].join("\n")}\n`);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return fail(`${colors.red}error:${colors.reset} ${apiMessage(error)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
package/dist/chat.d.ts
CHANGED
|
@@ -90,6 +90,21 @@ export declare function renderChatMessage(msg: ChatMessage, now: number): string
|
|
|
90
90
|
* capitalization is a display rule, and this line is for a program.
|
|
91
91
|
*/
|
|
92
92
|
export declare function chatMessageJson(msg: ChatMessage): string;
|
|
93
|
+
/**
|
|
94
|
+
* What `sfora chat <room>` does with the flags it got and the stdin it has.
|
|
95
|
+
*
|
|
96
|
+
* `-m` sends; `--follow` tails. Otherwise a terminal gets the interactive
|
|
97
|
+
* room (history, then type to talk), and anything else (an agent harness, a
|
|
98
|
+
* pipe, cron) gets the history and an exit. Without that last rule a
|
|
99
|
+
* non-TTY stdin held the process open waiting for EOF, and every line that
|
|
100
|
+
* did arrive on it was SENT to the room: reading must never post.
|
|
101
|
+
*/
|
|
102
|
+
export type ChatMode = "send" | "follow" | "read" | "interactive";
|
|
103
|
+
export declare function chatMode(opts: {
|
|
104
|
+
message?: string;
|
|
105
|
+
follow?: boolean;
|
|
106
|
+
stdinIsTty: boolean;
|
|
107
|
+
}): ChatMode;
|
|
93
108
|
/** `sfora rooms` — one row per room; unjoined rows carry their join hint. */
|
|
94
109
|
export declare function renderRoomList(rooms: Room[]): string;
|
|
95
110
|
/** Everything the tail loop needs from the outside world. */
|
package/dist/chat.js
CHANGED
|
@@ -214,6 +214,13 @@ export function chatMessageJson(msg) {
|
|
|
214
214
|
...(msg.sentVia ? { sentVia: msg.sentVia } : {}),
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
|
+
export function chatMode(opts) {
|
|
218
|
+
if (opts.message !== undefined)
|
|
219
|
+
return "send";
|
|
220
|
+
if (opts.follow)
|
|
221
|
+
return "follow";
|
|
222
|
+
return opts.stdinIsTty ? "interactive" : "read";
|
|
223
|
+
}
|
|
217
224
|
/** `sfora rooms` — one row per room; unjoined rows carry their join hint. */
|
|
218
225
|
export function renderRoomList(rooms) {
|
|
219
226
|
if (rooms.length === 0)
|
package/dist/cli-args.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export interface CliArgs {
|
|
9
9
|
skillsTarget?: string;
|
|
10
|
+
harness?: string;
|
|
11
|
+
dryRun: boolean;
|
|
10
12
|
skillVersion?: number;
|
|
11
13
|
expectedVersion?: number;
|
|
12
14
|
expectedRevision?: number;
|
|
@@ -40,5 +42,10 @@ export interface CliArgs {
|
|
|
40
42
|
option: string[];
|
|
41
43
|
target?: string;
|
|
42
44
|
room?: string;
|
|
45
|
+
stop: boolean;
|
|
46
|
+
out?: string;
|
|
47
|
+
webhookUrl?: string;
|
|
48
|
+
events?: string;
|
|
49
|
+
clear: boolean;
|
|
43
50
|
}
|
|
44
51
|
export declare function parseArgs(argv: string[]): CliArgs;
|
package/dist/cli-args.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* process — argv in, a parsed shape out.
|
|
7
7
|
*/
|
|
8
8
|
export function parseArgs(argv) {
|
|
9
|
-
const args = { rest: [], cwd: "/", mcp: false, help: false, draft: false, json: false, local: false, cloud: false, self: false, waitFlag: false, follow: false, awaitReply: false, option: [] };
|
|
9
|
+
const args = { rest: [], cwd: "/", mcp: false, help: false, draft: false, json: false, local: false, cloud: false, self: false, waitFlag: false, follow: false, awaitReply: false, option: [], stop: false, clear: false, dryRun: false };
|
|
10
10
|
for (let i = 0; i < argv.length; i++) {
|
|
11
11
|
const a = argv[i];
|
|
12
12
|
if (a === "--mcp")
|
|
@@ -15,6 +15,14 @@ export function parseArgs(argv) {
|
|
|
15
15
|
args.help = true;
|
|
16
16
|
else if (a === "--skills-target")
|
|
17
17
|
args.skillsTarget = argv[++i];
|
|
18
|
+
else if (a.startsWith("--skills-target="))
|
|
19
|
+
args.skillsTarget = a.slice("--skills-target=".length);
|
|
20
|
+
else if (a === "--harness")
|
|
21
|
+
args.harness = argv[++i];
|
|
22
|
+
else if (a.startsWith("--harness="))
|
|
23
|
+
args.harness = a.slice("--harness=".length);
|
|
24
|
+
else if (a === "--dry-run")
|
|
25
|
+
args.dryRun = true;
|
|
18
26
|
else if (a === "--version")
|
|
19
27
|
args.skillVersion = Number(argv[++i]);
|
|
20
28
|
else if (a === "--expected-version")
|
|
@@ -99,6 +107,8 @@ export function parseArgs(argv) {
|
|
|
99
107
|
args.limit = Number.parseInt(a.slice("--limit=".length), 10);
|
|
100
108
|
else if (a === "--follow")
|
|
101
109
|
args.follow = true;
|
|
110
|
+
else if (a === "--stop")
|
|
111
|
+
args.stop = true;
|
|
102
112
|
else if (a === "--await-reply")
|
|
103
113
|
args.awaitReply = true;
|
|
104
114
|
else if (a === "--timeout")
|
|
@@ -120,6 +130,16 @@ export function parseArgs(argv) {
|
|
|
120
130
|
args.target = argv[++i];
|
|
121
131
|
else if (a.startsWith("--for="))
|
|
122
132
|
args.target = a.slice("--for=".length);
|
|
133
|
+
else if (a === "--out")
|
|
134
|
+
args.out = argv[++i];
|
|
135
|
+
else if (a.startsWith("--out="))
|
|
136
|
+
args.out = a.slice("--out=".length);
|
|
137
|
+
else if (a === "--events")
|
|
138
|
+
args.events = argv[++i];
|
|
139
|
+
else if (a.startsWith("--events="))
|
|
140
|
+
args.events = a.slice("--events=".length);
|
|
141
|
+
else if (a === "--clear")
|
|
142
|
+
args.clear = true;
|
|
123
143
|
else if (a === "--room")
|
|
124
144
|
args.room = argv[++i];
|
|
125
145
|
else if (a.startsWith("--room="))
|
|
@@ -131,5 +151,12 @@ export function parseArgs(argv) {
|
|
|
131
151
|
args.rest.push(a);
|
|
132
152
|
}
|
|
133
153
|
}
|
|
154
|
+
// `sfora agent webhook <name> --url …`: the spec's --url is the WEBHOOK's
|
|
155
|
+
// address. It must not also become the API base (that would send the call
|
|
156
|
+
// to the receiver), so it moves; the API base comes from SFORA_URL/config.
|
|
157
|
+
if (args.command === "agent" && args.rest[0] === "webhook" && args.url !== undefined) {
|
|
158
|
+
args.webhookUrl = args.url;
|
|
159
|
+
args.url = undefined;
|
|
160
|
+
}
|
|
134
161
|
return args;
|
|
135
162
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,4 +6,15 @@
|
|
|
6
6
|
* SFORA_API_KEY=sk_… SFORA_URL=http://localhost:2222 sfora --org test
|
|
7
7
|
* sfora --mcp # MCP stdio server for Claude/Cursor
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The whole CLI for one argv (what follows `sfora`). Exported so a harness can
|
|
11
|
+
* drive the real command table in-process; the bin below calls it with
|
|
12
|
+
* `process.argv.slice(2)`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runCli(argv: string[]): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* True when this module is the process entry point. Compared through realpath:
|
|
17
|
+
* npm's bin shim and `npx` reach dist/cli.js through symlinks, and those must
|
|
18
|
+
* still run; an `import` of this module (a test, a harness) must not.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isEntryPoint(entry?: string | undefined, self?: string): boolean;
|