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 +21 -0
- package/README.md +130 -0
- package/dist/cli.js +381 -0
- package/dist/worker.js +460 -0
- package/docs/auth.md +50 -0
- package/docs/security.md +22 -0
- package/docs/self-hosting.md +62 -0
- package/examples/claude-code.md +19 -0
- package/examples/codex.md +21 -0
- package/examples/cron.sh +11 -0
- package/examples/github-actions.yml +24 -0
- package/examples/hermes.md +11 -0
- package/migrations/0001_initial.sql +88 -0
- package/migrations/0002_email_otp_auth.sql +21 -0
- package/package.json +49 -0
- package/wrangler.jsonc +35 -0
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
|
+
});
|