alif-fund 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alif
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # Alif CLI
2
+
3
+ Apply to Alif from your terminal, then let your coding agent keep your application updated with real traction.
4
+
5
+ Alif CLI is for founders using Codex, Claude Code, Hermes, Cursor agents, CI, cron, or custom scripts. You submit once, define the metric that matters, and your agent can keep that metric fresh.
6
+
7
+ ## Quickstart
8
+
9
+ Target command after npm publishing:
10
+
11
+ ```bash
12
+ npx alif-fund apply
13
+ ```
14
+
15
+ Packaging note: npm package names use the normal ASCII hyphen, so the package is `alif-fund`.
16
+
17
+ You can run directly from GitHub today:
18
+
19
+ ```bash
20
+ npx github:alifdotbuild/alifcli apply
21
+ ```
22
+
23
+ Apply:
24
+
25
+ ```bash
26
+ npx alif-fund apply
27
+ ```
28
+
29
+ The CLI will:
30
+
31
+ - send an email login code
32
+ - collect your company/application details
33
+ - create your primary metric
34
+ - save a local agent token
35
+ - print the command your agent should run next
36
+
37
+ Update traction:
38
+
39
+ ```bash
40
+ npx alif-fund metric update weekly_revenue 12000
41
+ ```
42
+
43
+ Check status:
44
+
45
+ ```bash
46
+ npx alif-fund status
47
+ ```
48
+
49
+ Generate an agent command:
50
+
51
+ ```bash
52
+ npx alif-fund setup-agent weekly_revenue
53
+ ```
54
+
55
+ ## Agent Usage
56
+
57
+ Agents and CI should use `ALIF_API_TOKEN`:
58
+
59
+ ```bash
60
+ ALIF_API_TOKEN=alif_live_... \
61
+ npx alif-fund metric update weekly_revenue 12000 \
62
+ --timestamp 2026-06-07T16:00:00Z \
63
+ --idempotency-key acme-weekly-revenue-2026-W23 \
64
+ --source codex
65
+ ```
66
+
67
+ Use the same idempotency key when retrying the same reporting period. Duplicate retries are ignored.
68
+
69
+ Examples:
70
+
71
+ - [Codex](examples/codex.md)
72
+ - [Claude Code](examples/claude-code.md)
73
+ - [Hermes](examples/hermes.md)
74
+ - [GitHub Actions](examples/github-actions.yml)
75
+ - [Cron](examples/cron.sh)
76
+
77
+ ## Commands
78
+
79
+ ```bash
80
+ npx alif-fund apply
81
+ npx alif-fund login --email founder@example.com
82
+ npx alif-fund status
83
+ npx alif-fund whoami
84
+ npx alif-fund setup-agent weekly_revenue
85
+ npx alif-fund metric create weekly_active_users --unit users --cadence weekly
86
+ npx alif-fund metric update weekly_revenue 12000
87
+ ```
88
+
89
+ ## Hosted API
90
+
91
+ By default, the CLI uses:
92
+
93
+ ```text
94
+ https://alif-api.imuthuvappa.workers.dev
95
+ ```
96
+
97
+ Override it when developing or self-hosting:
98
+
99
+ ```bash
100
+ ALIF_API_URL=http://localhost:8787 npx alif-fund apply
101
+ ```
102
+
103
+ ## Local Development
104
+
105
+ ```bash
106
+ npm install
107
+ npm run build
108
+ npm run db:migrate:local
109
+ npm run dev
110
+ ```
111
+
112
+ In another terminal:
113
+
114
+ ```bash
115
+ npm exec -- alif-fund apply --api-url http://localhost:8787
116
+ ```
117
+
118
+ ## Docs
119
+
120
+ - [Self-hosting on Cloudflare](docs/self-hosting.md)
121
+ - [Auth model](docs/auth.md)
122
+ - [Security notes](docs/security.md)
123
+
124
+ ## Status
125
+
126
+ This is an early MVP. The core loop works:
127
+
128
+ ```text
129
+ email login -> apply -> create metric -> agent updates traction -> detect growth spike
130
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stdout as output } from "node:process";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ const defaultApiUrl = "https://alif-api.imuthuvappa.workers.dev";
8
+ const configPath = join(homedir(), ".alif", "config.json");
9
+ async function main() {
10
+ const [command, ...args] = process.argv.slice(2);
11
+ if (!command || command === "help" || command === "--help") {
12
+ printHelp();
13
+ return;
14
+ }
15
+ if (command === "apply") {
16
+ await apply(args);
17
+ return;
18
+ }
19
+ if (command === "login") {
20
+ await login(args);
21
+ return;
22
+ }
23
+ if (command === "status") {
24
+ await status(args);
25
+ return;
26
+ }
27
+ if (command === "metric") {
28
+ await metric(args);
29
+ return;
30
+ }
31
+ if (command === "whoami") {
32
+ await whoami();
33
+ return;
34
+ }
35
+ if (command === "setup-agent") {
36
+ await setupAgent(args);
37
+ return;
38
+ }
39
+ throw new CliError(`Unknown command: ${command}`);
40
+ }
41
+ async function apply(args) {
42
+ const flags = parseFlags(args);
43
+ const existing = await loadConfig();
44
+ const rl = createInterface({ input, output });
45
+ const apiUrl = flags["api-url"] ?? process.env.ALIF_API_URL ?? existing.apiUrl ?? defaultApiUrl;
46
+ const founderEmail = (flags["founder-email"] ?? existing.email ?? await ask(rl, "Founder email")).toLowerCase();
47
+ const sessionConfig = await ensureSession({ apiUrl, email: founderEmail, flags, existing, rl });
48
+ const companyName = flags["company"] ?? await ask(rl, "Company name");
49
+ const website = flags.website ?? await ask(rl, "Website", "");
50
+ const founderName = flags["founder-name"] ?? await ask(rl, "Founder name");
51
+ const oneLiner = flags["one-liner"] ?? await ask(rl, "One-liner");
52
+ const metricKey = flags["metric-key"] ?? await ask(rl, "Primary metric key", "weekly_revenue");
53
+ const metricUnit = flags["metric-unit"] ?? await ask(rl, "Primary metric unit", "usd");
54
+ rl.close();
55
+ const signupSecret = flags["signup-secret"] ?? process.env.ALIF_SIGNUP_SECRET;
56
+ const sessionToken = flags["session-token"] ?? process.env.ALIF_SESSION_TOKEN ?? sessionConfig.sessionToken;
57
+ const response = await api(apiUrl, "/v1/applications", {
58
+ method: "POST",
59
+ token: sessionToken,
60
+ headers: signupSecret ? { "x-alif-signup-secret": signupSecret } : undefined,
61
+ body: {
62
+ company_name: companyName,
63
+ website: website || undefined,
64
+ founder_name: founderName,
65
+ founder_email: founderEmail,
66
+ narrative: { one_liner: oneLiner },
67
+ primary_metric: {
68
+ key: metricKey,
69
+ display_name: titleize(metricKey),
70
+ unit: metricUnit,
71
+ cadence: "weekly",
72
+ direction: "up"
73
+ }
74
+ }
75
+ });
76
+ const nextConfig = {
77
+ ...sessionConfig,
78
+ apiUrl,
79
+ email: founderEmail,
80
+ token: response.token,
81
+ companyId: response.company_id,
82
+ applicationId: response.application_id
83
+ };
84
+ await saveConfig(nextConfig);
85
+ console.log("");
86
+ console.log("Application submitted.");
87
+ console.log(`Application: ${response.application_id}`);
88
+ console.log(`Company: ${response.company_id}`);
89
+ console.log(`Primary metric: ${metricKey}`);
90
+ console.log("");
91
+ console.log("Update traction:");
92
+ console.log(` npx alif-fund metric update ${metricKey} 12000`);
93
+ console.log("");
94
+ console.log("For agents/CI:");
95
+ console.log(` export ALIF_API_TOKEN=${response.token}`);
96
+ console.log(` npx alif-fund metric update ${metricKey} 12000 --source <agent-name>`);
97
+ console.log("");
98
+ console.log(`Credentials saved to ${configPath}`);
99
+ }
100
+ async function login(args) {
101
+ const flags = parseFlags(args);
102
+ const existing = await loadConfig();
103
+ const rl = createInterface({ input, output });
104
+ const apiUrl = flags["api-url"] ?? process.env.ALIF_API_URL ?? existing.apiUrl ?? defaultApiUrl;
105
+ const email = (flags.email ?? existing.email ?? await ask(rl, "Email")).toLowerCase();
106
+ const nextConfig = await runEmailOtp({ apiUrl, email, flags, existing, rl });
107
+ rl.close();
108
+ await saveConfig(nextConfig);
109
+ console.log(`Logged in as ${email}`);
110
+ console.log(`Session saved to ${configPath}`);
111
+ }
112
+ async function status(args) {
113
+ const flags = parseFlags(args);
114
+ const config = await requireConfig(flags);
115
+ const response = await api(config.apiUrl, "/v1/status", {
116
+ method: "GET",
117
+ token: config.token
118
+ });
119
+ console.log(`${response.application.company_name} (${response.application.status})`);
120
+ console.log(`Application: ${response.application.id}`);
121
+ console.log("");
122
+ if (response.metrics.length === 0) {
123
+ console.log("No metrics yet.");
124
+ }
125
+ else {
126
+ for (const metric of response.metrics) {
127
+ const latest = metric.latest_value === null || metric.latest_value === undefined
128
+ ? "no points"
129
+ : `${metric.latest_value} ${metric.unit} at ${metric.latest_timestamp}`;
130
+ console.log(`${metric.key}: ${latest}`);
131
+ }
132
+ }
133
+ if (response.alerts.length > 0) {
134
+ console.log("");
135
+ console.log("Recent alerts:");
136
+ for (const alert of response.alerts) {
137
+ console.log(`[${alert.severity}] ${alert.summary}`);
138
+ }
139
+ }
140
+ }
141
+ async function metric(args) {
142
+ const [subcommand, ...rest] = args;
143
+ if (subcommand === "create") {
144
+ await createMetric(rest);
145
+ return;
146
+ }
147
+ if (subcommand === "update") {
148
+ await updateMetric(rest);
149
+ return;
150
+ }
151
+ throw new CliError("Expected `alif metric create` or `alif metric update`.");
152
+ }
153
+ async function createMetric(args) {
154
+ const flags = parseFlags(args);
155
+ const key = flags._[0] ?? flags.key;
156
+ if (!key)
157
+ throw new CliError("Metric key is required.");
158
+ const config = await requireConfig(flags);
159
+ const response = await api(config.apiUrl, "/v1/metrics", {
160
+ method: "POST",
161
+ token: config.token,
162
+ body: {
163
+ key,
164
+ display_name: flags.name ?? titleize(key),
165
+ unit: flags.unit ?? "count",
166
+ cadence: flags.cadence ?? "weekly",
167
+ direction: flags.direction ?? "up",
168
+ source_type: flags.source ?? "self_reported"
169
+ }
170
+ });
171
+ console.log(`Metric created: ${response.key} (${response.id})`);
172
+ }
173
+ async function updateMetric(args) {
174
+ const flags = parseFlags(args);
175
+ const key = flags._[0] ?? flags.key;
176
+ const rawValue = flags.value ?? flags._[1];
177
+ if (!key)
178
+ throw new CliError("Metric key is required.");
179
+ if (!rawValue)
180
+ throw new CliError("Metric value is required.");
181
+ const value = Number(rawValue);
182
+ if (!Number.isFinite(value))
183
+ throw new CliError("Metric value must be a number.");
184
+ const config = await requireConfig(flags);
185
+ const timestamp = flags.timestamp ?? new Date().toISOString();
186
+ const idempotencyKey = flags["idempotency-key"] ?? `${config.companyId ?? "company"}:${key}:${timestamp}`;
187
+ const response = await api(config.apiUrl, `/v1/metrics/${encodeURIComponent(key)}/points`, {
188
+ method: "POST",
189
+ token: config.token,
190
+ idempotencyKey,
191
+ body: {
192
+ value,
193
+ timestamp,
194
+ source: flags.source ?? "alif-cli",
195
+ confidence: flags.confidence ? Number(flags.confidence) : undefined,
196
+ raw_event_id: flags["raw-event-id"],
197
+ idempotency_key: idempotencyKey
198
+ }
199
+ });
200
+ console.log(response.duplicate ? `Duplicate ignored: ${response.id}` : `Metric point created: ${response.id}`);
201
+ }
202
+ async function whoami() {
203
+ const config = await loadConfig();
204
+ console.log(`API URL: ${config.apiUrl ?? "not configured"}`);
205
+ console.log(`Email: ${config.email ?? "not configured"}`);
206
+ console.log(`Session: ${config.sessionToken ? `configured until ${config.sessionExpiresAt ?? "unknown"}` : "not configured"}`);
207
+ console.log(`Company: ${config.companyId ?? "not configured"}`);
208
+ console.log(`Application: ${config.applicationId ?? "not configured"}`);
209
+ console.log(`Agent token: ${config.token ? "configured" : "not configured"}`);
210
+ }
211
+ async function setupAgent(args) {
212
+ const flags = parseFlags(args);
213
+ const config = await loadConfig();
214
+ const apiUrl = flags["api-url"] ?? process.env.ALIF_API_URL ?? config.apiUrl ?? defaultApiUrl;
215
+ const token = flags.token ?? process.env.ALIF_API_TOKEN ?? config.token;
216
+ const metric = flags.metric ?? flags._[0] ?? "weekly_revenue";
217
+ if (!token) {
218
+ throw new CliError("Missing agent token. Run `npx alif-fund apply` first, or pass --token / ALIF_API_TOKEN.");
219
+ }
220
+ console.log(`Agent setup
221
+
222
+ Use this command from Codex, Claude Code, Hermes, CI, or cron:
223
+
224
+ ALIF_API_URL=${apiUrl} \\
225
+ ALIF_API_TOKEN=${token} \\
226
+ npx alif-fund metric update ${metric} <value> \\
227
+ --timestamp <period_end_iso> \\
228
+ --idempotency-key <company>-${metric}-<period> \\
229
+ --source <agent-name>
230
+
231
+ Suggested agent instruction:
232
+
233
+ Calculate ${metric} from the source of truth for the reporting period. Then run the command above with a stable idempotency key for that period. If the command fails transiently, retry with the same idempotency key.
234
+ `);
235
+ }
236
+ async function api(apiUrl, path, options) {
237
+ if (!apiUrl)
238
+ throw new CliError("Missing API URL. Run `alif apply --api-url <url>` first.");
239
+ const headers = {
240
+ accept: "application/json",
241
+ ...options.headers
242
+ };
243
+ if (options.token)
244
+ headers.authorization = `Bearer ${options.token}`;
245
+ if (options.idempotencyKey)
246
+ headers["idempotency-key"] = options.idempotencyKey;
247
+ if (options.body)
248
+ headers["content-type"] = "application/json";
249
+ const response = await fetch(new URL(path, apiUrl), {
250
+ method: options.method,
251
+ headers,
252
+ body: options.body ? JSON.stringify(options.body) : undefined
253
+ });
254
+ const payload = await response.json().catch(() => ({}));
255
+ if (!response.ok) {
256
+ throw new CliError(payload.message ?? payload.error ?? `HTTP ${response.status}`);
257
+ }
258
+ return payload;
259
+ }
260
+ async function requireConfig(flags) {
261
+ const config = await loadConfig();
262
+ const apiUrl = flags["api-url"] ?? process.env.ALIF_API_URL ?? config.apiUrl ?? defaultApiUrl;
263
+ const token = flags.token ?? process.env.ALIF_API_TOKEN ?? config.token;
264
+ if (!token)
265
+ throw new CliError("Missing token. Pass --token, set ALIF_API_TOKEN, or run `alif apply`.");
266
+ return { ...config, apiUrl, token };
267
+ }
268
+ async function ensureSession(input) {
269
+ const explicit = input.flags["session-token"] ?? process.env.ALIF_SESSION_TOKEN;
270
+ if (explicit) {
271
+ return {
272
+ ...input.existing,
273
+ apiUrl: input.apiUrl,
274
+ email: input.email,
275
+ sessionToken: explicit
276
+ };
277
+ }
278
+ const existingSessionMatches = input.existing.sessionToken
279
+ && input.existing.email === input.email
280
+ && (!input.existing.sessionExpiresAt || Date.parse(input.existing.sessionExpiresAt) > Date.now() + 60_000);
281
+ if (existingSessionMatches) {
282
+ return { ...input.existing, apiUrl: input.apiUrl, email: input.email };
283
+ }
284
+ console.log(`Sending login code to ${input.email}`);
285
+ return runEmailOtp(input);
286
+ }
287
+ async function runEmailOtp(input) {
288
+ const start = await api(input.apiUrl, "/v1/auth/otp/start", {
289
+ method: "POST",
290
+ body: { email: input.email }
291
+ });
292
+ if (start.dev_otp) {
293
+ console.log(`Development OTP: ${start.dev_otp}`);
294
+ }
295
+ else {
296
+ console.log(`Sent login code to ${input.email}`);
297
+ }
298
+ const code = input.flags.code ?? await ask(input.rl, "OTP code");
299
+ const verified = await api(input.apiUrl, "/v1/auth/otp/verify", {
300
+ method: "POST",
301
+ body: { email: input.email, code }
302
+ });
303
+ return {
304
+ ...input.existing,
305
+ apiUrl: input.apiUrl,
306
+ email: input.email,
307
+ sessionToken: verified.session_token,
308
+ sessionExpiresAt: verified.expires_at
309
+ };
310
+ }
311
+ async function loadConfig() {
312
+ try {
313
+ return JSON.parse(await readFile(configPath, "utf8"));
314
+ }
315
+ catch {
316
+ return {};
317
+ }
318
+ }
319
+ async function saveConfig(config) {
320
+ await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
321
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
322
+ }
323
+ async function ask(rl, label, fallback) {
324
+ const suffix = fallback !== undefined ? ` [${fallback}]` : "";
325
+ const answer = (await rl.question(`${label}${suffix}: `)).trim();
326
+ if (answer)
327
+ return answer;
328
+ if (fallback !== undefined)
329
+ return fallback;
330
+ return ask(rl, label, fallback);
331
+ }
332
+ function parseFlags(args) {
333
+ const flags = { _: [] };
334
+ for (let i = 0; i < args.length; i += 1) {
335
+ const arg = args[i];
336
+ if (arg.startsWith("--")) {
337
+ const [name, inlineValue] = arg.slice(2).split("=", 2);
338
+ const next = args[i + 1];
339
+ if (inlineValue !== undefined) {
340
+ flags[name] = inlineValue;
341
+ }
342
+ else if (next && !next.startsWith("--")) {
343
+ flags[name] = next;
344
+ i += 1;
345
+ }
346
+ else {
347
+ flags[name] = "true";
348
+ }
349
+ }
350
+ else {
351
+ flags._.push(arg);
352
+ }
353
+ }
354
+ return flags;
355
+ }
356
+ function titleize(value) {
357
+ return value.replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
358
+ }
359
+ function printHelp() {
360
+ console.log(`alif-fund
361
+
362
+ Usage:
363
+ alif-fund apply
364
+ alif-fund login [--email founder@example.com]
365
+ alif-fund status
366
+ alif-fund whoami
367
+ alif-fund setup-agent [metric_key]
368
+ alif-fund metric create <key> [--unit count] [--cadence weekly]
369
+ alif-fund metric update <key> <value> [--timestamp ISO_DATE] [--source agent]
370
+
371
+ Automation:
372
+ ALIF_API_TOKEN=alif_live_... \\
373
+ alif-fund metric update weekly_revenue 12000
374
+ `);
375
+ }
376
+ class CliError extends Error {
377
+ }
378
+ main().catch((error) => {
379
+ console.error(error instanceof CliError ? error.message : error);
380
+ process.exitCode = 1;
381
+ });