flowsprite 0.1.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 +93 -0
- package/dist/bin.js +128 -0
- package/dist/cli.js +974 -0
- package/dist/credentials.js +195 -0
- package/dist/doctor.js +39 -0
- package/dist/endpoint.js +72 -0
- package/dist/login.js +154 -0
- package/dist/mcpClient.js +109 -0
- package/dist/mcpConfig.js +266 -0
- package/dist/mcpInstall.js +225 -0
- package/dist/messages.js +47 -0
- package/dist/output.js +21 -0
- package/dist/redact.js +47 -0
- package/dist/setup.js +216 -0
- package/dist/status.js +92 -0
- package/dist/usage.js +54 -0
- package/dist/version.js +10 -0
- package/package.json +21 -0
package/dist/setup.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { toolJson } from "./mcpClient.js";
|
|
2
|
+
/**
|
|
3
|
+
* The setup plane, driven from a terminal.
|
|
4
|
+
*
|
|
5
|
+
* THE ORDERING IS NOT RE-DERIVED HERE. `convex/mcpSetup.ts nextSetupAction` is
|
|
6
|
+
* the one home for "what is the next step", and `setup_status` returns its
|
|
7
|
+
* answer as `nextAction`. This module TRANSLATES that answer into a step this
|
|
8
|
+
* CLI can take; a second ladder computed from `workspace.salesforce.connected`
|
|
9
|
+
* and friends is exactly how the two start disagreeing.
|
|
10
|
+
*/
|
|
11
|
+
/** How often the link steps re-read `setup_status`, and for how long (AC5). */
|
|
12
|
+
export const LINK_POLL_INTERVAL_MS = 5_000;
|
|
13
|
+
export const LINK_POLL_MAX_MS = 10 * 60 * 1000;
|
|
14
|
+
/** `nextAction.tool` -> the step this CLI knows how to take. */
|
|
15
|
+
const TOOL_TO_STEP = {
|
|
16
|
+
setup_connect_salesforce: "connect_salesforce",
|
|
17
|
+
setup_connect_github: "connect_github",
|
|
18
|
+
setup_bind_repo: "bind_repo",
|
|
19
|
+
sfdc_trigger_sync: "first_sync",
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The two shape readers every module that touches a tool payload needs.
|
|
23
|
+
*
|
|
24
|
+
* ONE HOME, exported, because `status.ts` and `cli.ts` each grew their own and
|
|
25
|
+
* two copies of "is this an object" is how one of them starts disagreeing about
|
|
26
|
+
* an array.
|
|
27
|
+
*/
|
|
28
|
+
export function asObject(value) {
|
|
29
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
30
|
+
? value
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
/** The objects in an array field, dropping anything that is not one. */
|
|
34
|
+
export function asArray(value) {
|
|
35
|
+
if (!Array.isArray(value))
|
|
36
|
+
return [];
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const v of value) {
|
|
39
|
+
const o = asObject(v);
|
|
40
|
+
if (o !== null)
|
|
41
|
+
out.push(o);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* PURE. Given whatever `setup_status` returned, name the next step.
|
|
47
|
+
*
|
|
48
|
+
* FAIL-CLOSED DIRECTION IS `web_only`, NOT `done`. A payload this function
|
|
49
|
+
* cannot read — the tool's `null` (suspended tenant, or a session whose subject
|
|
50
|
+
* is no longer an active member), a `nextAction` naming a tool no version of
|
|
51
|
+
* this CLI drives, or `available: false` — must never be reported as "nothing
|
|
52
|
+
* left to do". Sending someone to /getting-started when setup is in fact
|
|
53
|
+
* finished costs them a page load; telling them setup is finished when it is not
|
|
54
|
+
* costs them the rest of the session.
|
|
55
|
+
*/
|
|
56
|
+
export function planNextStep(status) {
|
|
57
|
+
const obj = asObject(status);
|
|
58
|
+
const fallback = "Finish setting this workspace up in the browser, at /getting-started.";
|
|
59
|
+
if (obj === null)
|
|
60
|
+
return { kind: "web_only", text: fallback };
|
|
61
|
+
// `nextAction: null` is the server saying the ladder is EXHAUSTED — the one
|
|
62
|
+
// shape that legitimately means done. It is distinguished from an absent key,
|
|
63
|
+
// which means this payload is not a setup_status at all.
|
|
64
|
+
if (obj.nextAction === null)
|
|
65
|
+
return { kind: "done", text: "Setup is complete." };
|
|
66
|
+
const next = asObject(obj.nextAction);
|
|
67
|
+
if (next === null)
|
|
68
|
+
return { kind: "web_only", text: fallback };
|
|
69
|
+
const text = typeof next.text === "string" && next.text.length > 0 ? next.text : fallback;
|
|
70
|
+
// `available: false` means the step has no tool on this endpoint; the server
|
|
71
|
+
// already appended where to do it instead, so its text is the whole answer.
|
|
72
|
+
if (next.available !== true)
|
|
73
|
+
return { kind: "web_only", text };
|
|
74
|
+
const tool = typeof next.tool === "string" ? next.tool : "";
|
|
75
|
+
const kind = Object.prototype.hasOwnProperty.call(TOOL_TO_STEP, tool)
|
|
76
|
+
? TOOL_TO_STEP[tool]
|
|
77
|
+
: undefined;
|
|
78
|
+
if (kind === undefined)
|
|
79
|
+
return { kind: "web_only", text };
|
|
80
|
+
return { kind, text };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The connections the SERVER'S CURRENT RUNG is unsatisfied about.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ THE LADDER'S RUNGS ARE LIST-WIDE AND THIS CLI'S ACTIONS ARE PER-CONNECTION.
|
|
86
|
+
* `nextSetupAction` (`convex/mcpSetup.ts`) returns `setup_bind_repo` while
|
|
87
|
+
* `connections.some((c) => !c.hasBinding)` and `sfdc_trigger_sync` while
|
|
88
|
+
* `connections.some((c) => !c.hasSyncJob)` — deliberately, so an org with one
|
|
89
|
+
* bound and one unbound connection stays on the rung until EVERY connection is
|
|
90
|
+
* done. A caller that binds ONE connection and re-reads therefore gets the SAME
|
|
91
|
+
* rung back, which is not a failure and is not progress either.
|
|
92
|
+
*
|
|
93
|
+
* MEASURED at fd0f434: `setup` against a two-connection workspace made 201
|
|
94
|
+
* `setup_status` + 200 `setup_bind_repo` calls before an external cap stopped it,
|
|
95
|
+
* every one of those binds SUPERSEDING the binding it had just made and clearing
|
|
96
|
+
* that connection's data-export consent. So the rung's SUBJECT has to be derived
|
|
97
|
+
* here, and the loop in `cli.ts` has to prove the ladder moved.
|
|
98
|
+
*
|
|
99
|
+
* Returns the ids in payload order; an empty array means the payload does not
|
|
100
|
+
* explain the rung, which the caller treats as "cannot act", never as "any
|
|
101
|
+
* connection will do".
|
|
102
|
+
*/
|
|
103
|
+
export function rungConnectionIds(status, kind) {
|
|
104
|
+
const root = asObject(status);
|
|
105
|
+
if (root === null)
|
|
106
|
+
return [];
|
|
107
|
+
const term = kind === "bind_repo"
|
|
108
|
+
? (c) => c.hasBinding !== true
|
|
109
|
+
: kind === "first_sync"
|
|
110
|
+
? (c) => c.hasSyncJob !== true
|
|
111
|
+
: null;
|
|
112
|
+
if (term === null)
|
|
113
|
+
return [];
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const c of asArray(root.connections)) {
|
|
116
|
+
if (term(c) && typeof c.id === "string" && c.id.length > 0)
|
|
117
|
+
out.push(c.id);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Map the bind flags onto `setup_bind_repo`'s schema (`convex/mcpRegistry.ts`).
|
|
123
|
+
*
|
|
124
|
+
* `create` ALWAYS sends `isPrivate: true`, and there is no way to ask for a
|
|
125
|
+
* public one. That is not this CLI being opinionated: the tool "is refused" for
|
|
126
|
+
* anything else, because a public repo's GitHub Actions logs are world-readable.
|
|
127
|
+
* A flag that can only hold one value is not a choice to offer, so there is no
|
|
128
|
+
* `--private`; `--create` says it, and the usage text says so.
|
|
129
|
+
*/
|
|
130
|
+
export function planBindArgs(input) {
|
|
131
|
+
const connection = typeof input.connection === "string" ? input.connection.trim() : "";
|
|
132
|
+
if (connection.length === 0) {
|
|
133
|
+
return { ok: false, message: "bind needs --connection <salesforce connection id>. `flowsprite status` lists them." };
|
|
134
|
+
}
|
|
135
|
+
const rawInstallation = input.installation;
|
|
136
|
+
const installationId = typeof rawInstallation === "number"
|
|
137
|
+
? rawInstallation
|
|
138
|
+
: typeof rawInstallation === "string" && /^[0-9]+$/.test(rawInstallation.trim())
|
|
139
|
+
? Number(rawInstallation.trim())
|
|
140
|
+
: null;
|
|
141
|
+
if (installationId === null) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: "bind needs --installation <number>, the numeric GitHub installation id. `flowsprite status` lists them.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const wantsRepo = typeof input.repo === "string" && input.repo.length > 0;
|
|
148
|
+
const wantsCreate = typeof input.create === "string" && input.create.length > 0;
|
|
149
|
+
if (wantsRepo === wantsCreate) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
message: wantsRepo
|
|
153
|
+
? "bind takes either --repo owner/name or --create name, not both."
|
|
154
|
+
: "bind needs either --repo owner/name (bind a repo that exists) or --create name (make a new private one).",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (wantsCreate) {
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
args: { sandboxId: connection, installationId, repoName: input.create, create: true, isPrivate: true },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// `owner/name`, split on the FIRST slash only — a GitHub repo name cannot
|
|
164
|
+
// contain one, so anything after a second slash is a typo, not a path.
|
|
165
|
+
const parts = input.repo.split("/");
|
|
166
|
+
if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
|
|
167
|
+
return { ok: false, message: "--repo wants owner/name, for example acme/salesforce-metadata." };
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
args: { sandboxId: connection, installationId, repoOwner: parts[0], repoName: parts[1] },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Run one browser-leg step: mint the link, show it, then watch `setup_status`
|
|
176
|
+
* until the step is no longer the next one.
|
|
177
|
+
*
|
|
178
|
+
* THE STOP CONDITIONS ARE THREE AND THEY ARE DIFFERENT. The link's own
|
|
179
|
+
* `expiresAt` passing means the person must be handed a fresh link (this returns
|
|
180
|
+
* `expired` and says so); the overall budget running out means the CLI stops
|
|
181
|
+
* waiting but the link may still be good (`timeout`); and the step completing is
|
|
182
|
+
* read from the PLANNER moving on, not from a field this module interprets
|
|
183
|
+
* itself.
|
|
184
|
+
*/
|
|
185
|
+
export async function runLinkStep(deps, tool, args, stepKind) {
|
|
186
|
+
const minted = await deps.client.callTool(tool, args);
|
|
187
|
+
if (!minted.ok)
|
|
188
|
+
return { kind: "failed", message: minted.message };
|
|
189
|
+
const payload = asObject(toolJson(minted));
|
|
190
|
+
if (payload === null || typeof payload.url !== "string") {
|
|
191
|
+
return { kind: "failed", message: `${tool} did not return a link.` };
|
|
192
|
+
}
|
|
193
|
+
const url = payload.url;
|
|
194
|
+
const expiresAt = typeof payload.expiresAt === "number" ? payload.expiresAt : null;
|
|
195
|
+
deps.writer.out(`Open this link in a browser where you are signed in to Flowsprite:`);
|
|
196
|
+
deps.writer.out(` ${url}`);
|
|
197
|
+
if (typeof payload.instructions === "string")
|
|
198
|
+
deps.writer.out(payload.instructions);
|
|
199
|
+
deps.writer.out("Waiting for you to finish in the browser...");
|
|
200
|
+
const deadline = deps.now() + LINK_POLL_MAX_MS;
|
|
201
|
+
for (;;) {
|
|
202
|
+
await deps.sleep(LINK_POLL_INTERVAL_MS);
|
|
203
|
+
const status = await deps.client.callTool("setup_status", {});
|
|
204
|
+
if (!status.ok)
|
|
205
|
+
return { kind: "failed", message: status.message };
|
|
206
|
+
if (planNextStep(toolJson(status)).kind !== stepKind)
|
|
207
|
+
return { kind: "done" };
|
|
208
|
+
// The link check comes AFTER the status read, so a link that expired in the
|
|
209
|
+
// same tick as the step completing is reported as the completion it was.
|
|
210
|
+
const now = deps.now();
|
|
211
|
+
if (expiresAt !== null && now >= expiresAt)
|
|
212
|
+
return { kind: "expired" };
|
|
213
|
+
if (now >= deadline)
|
|
214
|
+
return { kind: "timeout" };
|
|
215
|
+
}
|
|
216
|
+
}
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { asArray, asObject, planNextStep } from "./setup.js";
|
|
2
|
+
/**
|
|
3
|
+
* Render a `setup_status` payload as plain text.
|
|
4
|
+
*
|
|
5
|
+
* READ DEFENSIVELY, FIELD BY FIELD. The payload is the tool's, not this
|
|
6
|
+
* module's: a missing section prints as "not reported" rather than throwing, so
|
|
7
|
+
* a server that gains or loses a field cannot turn `flowsprite status` into a
|
|
8
|
+
* stack trace. `--json` does not come through here at all — the machine form is
|
|
9
|
+
* the payload itself, unmodified, so a consumer is never reading this file's
|
|
10
|
+
* idea of it.
|
|
11
|
+
*/
|
|
12
|
+
const obj = asObject;
|
|
13
|
+
const arr = asArray;
|
|
14
|
+
function str(value, fallback = "?") {
|
|
15
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
16
|
+
}
|
|
17
|
+
function yesNo(value) {
|
|
18
|
+
return value === true ? "yes" : value === false ? "no" : "not reported";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A `pendingLinks` expiry, as text.
|
|
22
|
+
*
|
|
23
|
+
* `expiresAt` was the ONE field this renderer read without a helper, and
|
|
24
|
+
* `new Date(Number(x)).toISOString()` THROWS `RangeError: Invalid time value` on
|
|
25
|
+
* anything that is not a finite epoch — measured: `undefined`, an ISO string, and
|
|
26
|
+
* `Infinity` each crashed `flowsprite status` outright. Everything else here
|
|
27
|
+
* degrades to "not reported" when the payload surprises it, and so does this: a
|
|
28
|
+
* status command that cannot survive one odd field is not a status command.
|
|
29
|
+
* `Number.isFinite` is the whole rule (it is false for NaN and both infinities),
|
|
30
|
+
* plus a range check, because a finite number outside the Date epoch range
|
|
31
|
+
* (|t| > 8.64e15) throws exactly the same way.
|
|
32
|
+
*/
|
|
33
|
+
const MAX_EPOCH_MS = 8.64e15;
|
|
34
|
+
function expiryText(value) {
|
|
35
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > MAX_EPOCH_MS) {
|
|
36
|
+
return "not reported";
|
|
37
|
+
}
|
|
38
|
+
return new Date(value).toISOString();
|
|
39
|
+
}
|
|
40
|
+
export function renderStatus(payload) {
|
|
41
|
+
const root = obj(payload);
|
|
42
|
+
if (root === null) {
|
|
43
|
+
return [
|
|
44
|
+
"Flowsprite could not read this workspace's setup.",
|
|
45
|
+
"That is what it answers when the workspace is suspended, or when your membership is no longer active.",
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
const lines = [];
|
|
49
|
+
const workspace = obj(root.workspace);
|
|
50
|
+
const salesforce = obj(workspace?.salesforce);
|
|
51
|
+
const github = obj(workspace?.github);
|
|
52
|
+
const personal = obj(obj(root.personal)?.mcp);
|
|
53
|
+
const progress = obj(root.progress);
|
|
54
|
+
lines.push("Checklist");
|
|
55
|
+
lines.push(` Salesforce connected: ${yesNo(salesforce?.connected)}${salesforce?.summary ? ` (${str(salesforce.summary)})` : ""}`);
|
|
56
|
+
lines.push(` GitHub connected: ${yesNo(github?.connected)}${github?.summary ? ` (${str(github.summary)})` : ""}`);
|
|
57
|
+
lines.push(` Your MCP access: ${str(personal?.state, "not reported")}`);
|
|
58
|
+
if (progress && typeof progress.done === "number" && typeof progress.total === "number") {
|
|
59
|
+
lines.push(` Progress: ${progress.done}/${progress.total}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push(` Complete: ${yesNo(root.complete)}`);
|
|
62
|
+
const connections = arr(root.connections);
|
|
63
|
+
lines.push("", `Salesforce connections (${connections.length})`);
|
|
64
|
+
if (connections.length === 0)
|
|
65
|
+
lines.push(" none");
|
|
66
|
+
for (const c of connections) {
|
|
67
|
+
lines.push(` ${str(c.id)} ${str(c.displayName)} [${str(c.envType)}] repo: ${str(c.repo, "none")} synced: ${yesNo(c.hasSyncJob)}`);
|
|
68
|
+
}
|
|
69
|
+
const installations = arr(root.installations);
|
|
70
|
+
lines.push("", `GitHub installations (${installations.length})`);
|
|
71
|
+
if (installations.length === 0)
|
|
72
|
+
lines.push(" none");
|
|
73
|
+
for (const i of installations) {
|
|
74
|
+
const repos = arr(i.repos).map((r) => str(r.fullName));
|
|
75
|
+
lines.push(` ${String(i.installationId ?? "?")} @${str(i.accountLogin)} [${str(i.accountType)}] repos listed: ${repos.length}`);
|
|
76
|
+
// The bind step needs these names, and a truncated list would make a repo
|
|
77
|
+
// that IS bindable read as absent — so every one is printed, one per line.
|
|
78
|
+
for (const name of repos)
|
|
79
|
+
lines.push(` ${name}`);
|
|
80
|
+
}
|
|
81
|
+
const pending = arr(root.pendingLinks);
|
|
82
|
+
if (pending.length > 0) {
|
|
83
|
+
lines.push("", `Setup links still open (${pending.length})`);
|
|
84
|
+
for (const p of pending) {
|
|
85
|
+
lines.push(` ${str(p.purpose)} — expires ${expiryText(p.expiresAt)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const step = planNextStep(root);
|
|
89
|
+
lines.push("", "Next step");
|
|
90
|
+
lines.push(` ${step.text}`);
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** The usage text, as data, so `--help` and every exit-2 refusal print the same thing. */
|
|
2
|
+
export const USAGE = `flowsprite — set up and drive a Flowsprite workspace from a terminal.
|
|
3
|
+
|
|
4
|
+
Usage: flowsprite [command] [options]
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
setup Run the whole setup ladder (the default).
|
|
8
|
+
login Log this terminal in with a device code.
|
|
9
|
+
logout Forget the stored credential for this endpoint.
|
|
10
|
+
status Show the setup checklist, connections and installations.
|
|
11
|
+
connect salesforce Get a one-time link to connect a Salesforce org.
|
|
12
|
+
connect github Get a one-time link to install the GitHub app.
|
|
13
|
+
bind Bind a GitHub repo to a Salesforce connection. With no
|
|
14
|
+
--repo/--create it offers the repos setup_status lists.
|
|
15
|
+
sync Start a metadata sync and show the recent sync log.
|
|
16
|
+
doctor Check node, the endpoint, the credential, and sf/gh.
|
|
17
|
+
mcp install Point an AI client at this workspace's MCP endpoint.
|
|
18
|
+
mcp print Show the config snippet for a client, token hidden.
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--endpoint <origin> Flowsprite origin (default https://app.flowsprite.com).
|
|
22
|
+
--json Machine-readable output where a command has one.
|
|
23
|
+
--token <token> Use this token instead of the stored credential.
|
|
24
|
+
--save With --token: also store it for this endpoint.
|
|
25
|
+
--no-open Do not try to open a browser during login.
|
|
26
|
+
--sandbox connect salesforce: use the sandbox login host.
|
|
27
|
+
--my-domain <host> connect salesforce: use this My Domain login host.
|
|
28
|
+
--connection <id> bind / sync: the Salesforce connection id.
|
|
29
|
+
--installation <number> bind: the GitHub installation id.
|
|
30
|
+
--repo <owner/name> bind: a repository that already exists.
|
|
31
|
+
--create <name> bind: create a new repository instead. Always PRIVATE —
|
|
32
|
+
Flowsprite only mirrors into private repos.
|
|
33
|
+
--client <name> mcp: claude-code, codex, claude-desktop or cursor.
|
|
34
|
+
Omitted, mcp install offers the ones it detects and
|
|
35
|
+
mcp print shows every one.
|
|
36
|
+
--print mcp install: show what it would do, change nothing.
|
|
37
|
+
--force mcp install: replace an existing flowsprite entry.
|
|
38
|
+
--reveal mcp print: print the real token instead of
|
|
39
|
+
<YOUR_TOKEN>. Only at a terminal, never through a pipe.
|
|
40
|
+
--token-env <NAME> mcp install: codex reads the bearer from this
|
|
41
|
+
variable; claude-code uses its CURRENT value.
|
|
42
|
+
-h, --help Show this help and exit 0.
|
|
43
|
+
-v, --version Show the version and exit 0.
|
|
44
|
+
|
|
45
|
+
Environment:
|
|
46
|
+
FLOWSPRITE_ENDPOINT Same as --endpoint.
|
|
47
|
+
FLOWSPRITE_TOKEN Same as --token.
|
|
48
|
+
FLOWSPRITE_CONFIG_DIR Where credentials.json lives (default
|
|
49
|
+
$XDG_CONFIG_HOME/flowsprite or ~/.config/flowsprite).
|
|
50
|
+
|
|
51
|
+
The credential file holds one token per endpoint, mode 0600 in a 0700 directory.
|
|
52
|
+
mcp install backs a client config up once before its first write and changes
|
|
53
|
+
only the flowsprite entry. The ONLY command that prints a token is
|
|
54
|
+
mcp print --reveal, and only at a terminal.`;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's version, as one exported constant.
|
|
3
|
+
*
|
|
4
|
+
* NOT read from package.json at runtime: `dist/bin.js` sits one directory below
|
|
5
|
+
* the manifest, and an installed package's layout is npm's business, not ours —
|
|
6
|
+
* a path walk is a second authority that can drift. The constant and the
|
|
7
|
+
* manifest are pinned EQUAL by cli/src/packaging.test.ts, which reads both, so
|
|
8
|
+
* they cannot disagree without a red test.
|
|
9
|
+
*/
|
|
10
|
+
export const VERSION = "0.1.0";
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flowsprite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The Flowsprite CLI: log a terminal in to Flowsprite and drive workspace setup over the MCP endpoint. Zero runtime dependencies, node builtins only.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"homepage": "https://app.flowsprite.com/setup",
|
|
8
|
+
"bin": {
|
|
9
|
+
"flowsprite": "./dist/bin.js"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
}
|
|
21
|
+
}
|