alphamilk 0.0.3 → 0.0.5
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 +146 -0
- package/dist/cli.js +247 -75
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# alphamilk
|
|
2
|
+
|
|
3
|
+
AI-powered SEO research from your terminal. A thin CLI proxy to the Alpha Milk Worker API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run directly without installing
|
|
9
|
+
npx alphamilk --help
|
|
10
|
+
|
|
11
|
+
# Or install globally
|
|
12
|
+
npm install -g alphamilk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Authentication
|
|
16
|
+
|
|
17
|
+
All commands (except `login` and `logout`) require an active session. Authenticate with your access token:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
alphamilk login --token <your-token-id>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This creates a session stored at `~/.config/alphamilk/session.json`. Sessions expire after 24 hours by default.
|
|
24
|
+
|
|
25
|
+
Check your current session:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
alphamilk session
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Clear your session:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
alphamilk logout
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
### `login` -- Authenticate with your access token
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
alphamilk login --token <tokenId>
|
|
43
|
+
alphamilk login -t <tokenId>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Calls `GET /milk/create-session/<tokenId>` and stores the returned session locally.
|
|
47
|
+
|
|
48
|
+
### `logout` -- Clear your session
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
alphamilk logout
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Removes the local session file.
|
|
55
|
+
|
|
56
|
+
### `session` -- Show current session info
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
alphamilk session
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Displays the session ID, token (truncated), base URL, and expiry.
|
|
63
|
+
|
|
64
|
+
### `playbooks` -- List available research playbooks
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
alphamilk playbooks
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `playbook` -- Open a playbook or its report template
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Get playbook content
|
|
74
|
+
alphamilk playbook competitor-discovery
|
|
75
|
+
|
|
76
|
+
# Get the report template for a playbook
|
|
77
|
+
alphamilk playbook competitor-discovery --report
|
|
78
|
+
alphamilk playbook competitor-discovery -r
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `probe` -- Execute a research probe
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
alphamilk probe serp-google -p keyword=seo -p location_code=2840
|
|
85
|
+
alphamilk probe ranked-keywords --param target=example.com
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Parameters are passed as repeatable `--param key=value` (or `-p key=value`) pairs. The probe is executed via `POST /milk/probe/<sessionId>/<slug>` with parameters sent as a JSON body.
|
|
89
|
+
|
|
90
|
+
### `report` -- Save and retrieve research reports
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Save a report from a file (recommended)
|
|
94
|
+
alphamilk report save --name "Competitor Analysis" --file ./report.md
|
|
95
|
+
|
|
96
|
+
# Save a report with inline content
|
|
97
|
+
alphamilk report save --name "Quick Note" --content "# Summary\n\nKey findings..."
|
|
98
|
+
|
|
99
|
+
# Retrieve a saved report
|
|
100
|
+
alphamilk report get <reportId>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You must provide exactly one of `--file` or `--content` when saving.
|
|
104
|
+
|
|
105
|
+
### `artifact` -- Save and retrieve research artifacts
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Save an artifact
|
|
109
|
+
alphamilk artifact save --type domains --tags "competitors,direct" --data "example.com,other.com"
|
|
110
|
+
|
|
111
|
+
# List artifacts with optional filters
|
|
112
|
+
alphamilk artifact list
|
|
113
|
+
alphamilk artifact list --type domains
|
|
114
|
+
alphamilk artifact list --tag competitors
|
|
115
|
+
|
|
116
|
+
# Retrieve a specific artifact
|
|
117
|
+
alphamilk artifact get <artifactId>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Artifact types are `domains` or `keywords`. Tags and data are comma-separated.
|
|
121
|
+
|
|
122
|
+
## How It Works
|
|
123
|
+
|
|
124
|
+
The CLI is a thin HTTP proxy. Every command translates to a GET or POST request to the Alpha Milk Worker API at `https://alphamilk.ai/milk/*`. Responses (typically markdown) are printed directly to stdout.
|
|
125
|
+
|
|
126
|
+
All requests include two custom headers for server-side identification:
|
|
127
|
+
- `X-AlphaMilk-Client: cli`
|
|
128
|
+
- `X-AlphaMilk-CLI-Version: <version>`
|
|
129
|
+
|
|
130
|
+
## Build
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Build the single-file bundle
|
|
134
|
+
pnpm build
|
|
135
|
+
|
|
136
|
+
# Run in development mode (via tsx)
|
|
137
|
+
pnpm dev
|
|
138
|
+
|
|
139
|
+
# Type check
|
|
140
|
+
pnpm check
|
|
141
|
+
|
|
142
|
+
# Run tests
|
|
143
|
+
pnpm test
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The build uses esbuild to bundle all source into a single `dist/cli.js` file with a Node.js shebang prepended.
|
package/dist/cli.js
CHANGED
|
@@ -44,7 +44,7 @@ function clearSession() {
|
|
|
44
44
|
// src/api.ts
|
|
45
45
|
import { Effect, Data } from "effect";
|
|
46
46
|
import { HttpClient, HttpClientRequest } from "@effect/platform";
|
|
47
|
-
var CLI_VERSION = "0.0.
|
|
47
|
+
var CLI_VERSION = "0.0.3";
|
|
48
48
|
var ApiError = class extends Data.TaggedError("ApiError") {
|
|
49
49
|
};
|
|
50
50
|
var apiGet = (baseUrl, path3, params) => Effect.gen(function* () {
|
|
@@ -68,6 +68,21 @@ var apiGet = (baseUrl, path3, params) => Effect.gen(function* () {
|
|
|
68
68
|
}
|
|
69
69
|
return body;
|
|
70
70
|
});
|
|
71
|
+
var apiPut = (baseUrl, path3, body) => Effect.gen(function* () {
|
|
72
|
+
const client = yield* HttpClient.HttpClient;
|
|
73
|
+
const url = new URL(path3, baseUrl);
|
|
74
|
+
const request = HttpClientRequest.put(url.toString()).pipe(
|
|
75
|
+
HttpClientRequest.setHeader("X-AlphaMilk-Client", "cli"),
|
|
76
|
+
HttpClientRequest.setHeader("X-AlphaMilk-CLI-Version", CLI_VERSION),
|
|
77
|
+
HttpClientRequest.bodyUnsafeJson(body)
|
|
78
|
+
);
|
|
79
|
+
const response = yield* client.execute(request).pipe(Effect.scoped);
|
|
80
|
+
const responseBody = yield* response.text;
|
|
81
|
+
if (response.status >= 400) {
|
|
82
|
+
return yield* Effect.fail(new ApiError({ statusCode: response.status, message: responseBody }));
|
|
83
|
+
}
|
|
84
|
+
return responseBody;
|
|
85
|
+
});
|
|
71
86
|
var apiPost = (baseUrl, path3, body) => Effect.gen(function* () {
|
|
72
87
|
const client = yield* HttpClient.HttpClient;
|
|
73
88
|
const url = new URL(path3, baseUrl);
|
|
@@ -217,18 +232,30 @@ var playbookReport = Options.boolean("report").pipe(
|
|
|
217
232
|
Options.withDefault(false),
|
|
218
233
|
Options.withDescription("Get the report template instead of playbook content")
|
|
219
234
|
);
|
|
235
|
+
var playbookStep = Options.text("step").pipe(
|
|
236
|
+
Options.withAlias("s"),
|
|
237
|
+
Options.withDescription("Get a single step's instructions by step ID"),
|
|
238
|
+
Options.optional
|
|
239
|
+
);
|
|
220
240
|
var playbookCommand = Command.make(
|
|
221
241
|
"playbook",
|
|
222
|
-
{ slug: playbookSlug, report: playbookReport },
|
|
223
|
-
({ slug, report }) => Effect3.gen(function* () {
|
|
242
|
+
{ slug: playbookSlug, report: playbookReport, step: playbookStep },
|
|
243
|
+
({ slug, report, step }) => Effect3.gen(function* () {
|
|
224
244
|
const session = yield* requireSession;
|
|
225
|
-
|
|
245
|
+
let urlPath;
|
|
246
|
+
if (report) {
|
|
247
|
+
urlPath = `/milk/playbook/${session.sessionId}/${slug}/report`;
|
|
248
|
+
} else if (step._tag === "Some") {
|
|
249
|
+
urlPath = `/milk/playbook/${session.sessionId}/${slug}?step=${encodeURIComponent(step.value)}`;
|
|
250
|
+
} else {
|
|
251
|
+
urlPath = `/milk/playbook/${session.sessionId}/${slug}`;
|
|
252
|
+
}
|
|
226
253
|
const response = yield* apiGet(session.baseUrl, urlPath).pipe(
|
|
227
254
|
handleApiError("Failed to get playbook")
|
|
228
255
|
);
|
|
229
256
|
yield* Console2.log(response);
|
|
230
257
|
})
|
|
231
|
-
).pipe(Command.withDescription("Open a playbook or its report template"));
|
|
258
|
+
).pipe(Command.withDescription("Open a playbook, a specific step, or its report template"));
|
|
232
259
|
var probeSlug = Args.text({ name: "slug" }).pipe(
|
|
233
260
|
Args.withDescription("Probe slug (e.g., serp-google, ranked-keywords)")
|
|
234
261
|
);
|
|
@@ -252,99 +279,153 @@ var probeCommand = Command.make(
|
|
|
252
279
|
session.baseUrl,
|
|
253
280
|
`/milk/probe/${session.sessionId}/${slug}`,
|
|
254
281
|
queryParams
|
|
255
|
-
).pipe(handleApiError("
|
|
282
|
+
).pipe(handleApiError("Probe execution failed"));
|
|
256
283
|
yield* Console2.log(response);
|
|
257
284
|
})
|
|
258
285
|
).pipe(Command.withDescription("Execute a research probe"));
|
|
259
|
-
var
|
|
286
|
+
var artifactSaveType = Options.choice("type", ["domains", "keywords", "document", "report", "metrics"]).pipe(
|
|
287
|
+
Options.withDescription("Artifact type")
|
|
288
|
+
);
|
|
289
|
+
var artifactSaveTitle = Options.text("title").pipe(
|
|
260
290
|
Options.withAlias("n"),
|
|
261
|
-
Options.withDescription("
|
|
291
|
+
Options.withDescription("Artifact title (required for document/report)"),
|
|
292
|
+
Options.optional
|
|
262
293
|
);
|
|
263
|
-
var
|
|
264
|
-
Options.
|
|
265
|
-
|
|
294
|
+
var artifactSaveTags = Options.text("tags").pipe(
|
|
295
|
+
Options.withDescription("Comma-separated tags (e.g., competitors,direct)")
|
|
296
|
+
);
|
|
297
|
+
var artifactSaveData = Options.text("data").pipe(
|
|
298
|
+
Options.withAlias("d"),
|
|
299
|
+
Options.withDescription("Comma-separated data items (for domains/keywords)"),
|
|
266
300
|
Options.optional
|
|
267
301
|
);
|
|
268
|
-
var
|
|
302
|
+
var artifactSaveFile = Options.text("file").pipe(
|
|
269
303
|
Options.withAlias("f"),
|
|
270
|
-
Options.withDescription("Path to a markdown
|
|
304
|
+
Options.withDescription("Path to a file (markdown for document, JSON for report/metrics)"),
|
|
305
|
+
Options.optional
|
|
306
|
+
);
|
|
307
|
+
var artifactSaveContent = Options.text("content").pipe(
|
|
308
|
+
Options.withAlias("c"),
|
|
309
|
+
Options.withDescription("Inline content string (for document type)"),
|
|
271
310
|
Options.optional
|
|
272
311
|
);
|
|
273
|
-
var
|
|
312
|
+
var artifactSaveCommand = Command.make(
|
|
274
313
|
"save",
|
|
275
|
-
{
|
|
276
|
-
({
|
|
314
|
+
{ type: artifactSaveType, title: artifactSaveTitle, tags: artifactSaveTags, data: artifactSaveData, file: artifactSaveFile, content: artifactSaveContent },
|
|
315
|
+
({ type, title, tags, data, file, content }) => Effect3.gen(function* () {
|
|
277
316
|
const session = yield* requireSession;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (content._tag === "None" && file._tag === "None") {
|
|
285
|
-
return yield* exitWithError(
|
|
286
|
-
'Missing report content. Provide one of:\n\n --file report.md Read content from a markdown file (recommended)\n --content "# ..." Pass content inline as a string\n\nExample:\n alphamilk report save --name "My Report" --file ./report.md'
|
|
287
|
-
);
|
|
317
|
+
const body = {
|
|
318
|
+
type,
|
|
319
|
+
tags: tags.split(",").map((t) => t.trim())
|
|
320
|
+
};
|
|
321
|
+
if (title._tag === "Some") {
|
|
322
|
+
body.title = title.value;
|
|
288
323
|
}
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
324
|
+
if (type === "domains" || type === "keywords") {
|
|
325
|
+
if (data._tag === "None") {
|
|
326
|
+
return yield* exitWithError(
|
|
327
|
+
`--data is required for ${type} artifacts.
|
|
328
|
+
|
|
329
|
+
Example: alphamilk artifact save --type ${type} --tags step:discovery --data "example.com,other.com"`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
body.data = data.value.split(",").map((d) => d.trim());
|
|
333
|
+
} else if (type === "document") {
|
|
334
|
+
if (file._tag === "None" && content._tag === "None") {
|
|
335
|
+
return yield* exitWithError(
|
|
336
|
+
'Document artifacts require --file or --content.\n\nExample:\n alphamilk artifact save --type document --title "Analysis" --tags step:analysis --file ./analysis.md\n alphamilk artifact save --type document --title "Notes" --tags notes --content "# Notes\\n..."'
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (file._tag === "Some" && content._tag === "Some") {
|
|
340
|
+
return yield* exitWithError("Cannot use both --file and --content. Provide one or the other.");
|
|
341
|
+
}
|
|
342
|
+
if (file._tag === "Some") {
|
|
343
|
+
body.content = yield* readReportFile(file.value);
|
|
344
|
+
} else {
|
|
345
|
+
body.content = content.value;
|
|
346
|
+
}
|
|
347
|
+
} else if (type === "report") {
|
|
348
|
+
if (file._tag === "None") {
|
|
349
|
+
return yield* exitWithError(
|
|
350
|
+
'Report artifacts require --file with a JSON manifest.\n\nExample: alphamilk artifact save --type report --title "Final Report" --tags report --file ./manifest.json'
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
body.content = yield* readReportFile(file.value);
|
|
354
|
+
} else if (type === "metrics") {
|
|
355
|
+
if (file._tag === "Some") {
|
|
356
|
+
body.content = yield* readReportFile(file.value);
|
|
357
|
+
} else if (data._tag === "Some") {
|
|
358
|
+
const rows = data.value.split(",").map((pair) => {
|
|
359
|
+
const [label, rest] = pair.trim().split(":");
|
|
360
|
+
const value = parseFloat(rest ?? "0");
|
|
361
|
+
return { label: label?.trim() ?? "", value: isNaN(value) ? 0 : value };
|
|
362
|
+
});
|
|
363
|
+
body.content = JSON.stringify(rows);
|
|
364
|
+
} else {
|
|
365
|
+
return yield* exitWithError(
|
|
366
|
+
'Metrics artifacts require --file (JSON) or --data (label:value pairs).\n\nExample:\n alphamilk artifact save --type metrics --tags traffic --file ./metrics.json\n alphamilk artifact save --type metrics --tags traffic --data "example.com:45000,other.com:12000"'
|
|
367
|
+
);
|
|
368
|
+
}
|
|
293
369
|
}
|
|
294
370
|
const response = yield* apiPost(
|
|
295
371
|
session.baseUrl,
|
|
296
|
-
`/milk/
|
|
297
|
-
|
|
298
|
-
).pipe(handleApiError("Failed to save
|
|
372
|
+
`/milk/artifact/${session.sessionId}`,
|
|
373
|
+
body
|
|
374
|
+
).pipe(handleApiError("Failed to save artifact"));
|
|
299
375
|
yield* Console2.log(response);
|
|
300
376
|
})
|
|
301
|
-
).pipe(Command.withDescription("Save a research
|
|
302
|
-
var
|
|
303
|
-
Args.withDescription("
|
|
377
|
+
).pipe(Command.withDescription("Save a research artifact"));
|
|
378
|
+
var artifactUpdateId = Args.text({ name: "artifactId" }).pipe(
|
|
379
|
+
Args.withDescription("Artifact ID to update")
|
|
304
380
|
);
|
|
305
|
-
var
|
|
306
|
-
"
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const session = yield* requireSession;
|
|
310
|
-
const response = yield* apiGet(
|
|
311
|
-
session.baseUrl,
|
|
312
|
-
`/milk/report/${session.sessionId}/${reportId}`
|
|
313
|
-
).pipe(handleApiError("Failed to get report"));
|
|
314
|
-
yield* Console2.log(response);
|
|
315
|
-
})
|
|
316
|
-
).pipe(Command.withDescription("Retrieve a saved report"));
|
|
317
|
-
var reportCommand = Command.make("report").pipe(
|
|
318
|
-
Command.withDescription("Save and retrieve research reports"),
|
|
319
|
-
Command.withSubcommands([reportSaveCommand, reportGetCommand])
|
|
381
|
+
var artifactUpdateTitle = Options.text("title").pipe(
|
|
382
|
+
Options.withAlias("n"),
|
|
383
|
+
Options.withDescription("Updated title"),
|
|
384
|
+
Options.optional
|
|
320
385
|
);
|
|
321
|
-
var
|
|
322
|
-
Options.
|
|
386
|
+
var artifactUpdateFile = Options.text("file").pipe(
|
|
387
|
+
Options.withAlias("f"),
|
|
388
|
+
Options.withDescription("Path to updated content file"),
|
|
389
|
+
Options.optional
|
|
323
390
|
);
|
|
324
|
-
var
|
|
325
|
-
Options.
|
|
391
|
+
var artifactUpdateContent = Options.text("content").pipe(
|
|
392
|
+
Options.withAlias("c"),
|
|
393
|
+
Options.withDescription("Updated inline content"),
|
|
394
|
+
Options.optional
|
|
326
395
|
);
|
|
327
|
-
var
|
|
396
|
+
var artifactUpdateData = Options.text("data").pipe(
|
|
328
397
|
Options.withAlias("d"),
|
|
329
|
-
Options.withDescription("
|
|
398
|
+
Options.withDescription("Updated comma-separated data items"),
|
|
399
|
+
Options.optional
|
|
330
400
|
);
|
|
331
|
-
var
|
|
332
|
-
"
|
|
333
|
-
{
|
|
334
|
-
({
|
|
401
|
+
var artifactUpdateCommand = Command.make(
|
|
402
|
+
"update",
|
|
403
|
+
{ artifactId: artifactUpdateId, title: artifactUpdateTitle, file: artifactUpdateFile, content: artifactUpdateContent, data: artifactUpdateData },
|
|
404
|
+
({ artifactId, title, file, content, data }) => Effect3.gen(function* () {
|
|
335
405
|
const session = yield* requireSession;
|
|
336
|
-
const
|
|
406
|
+
const body = {};
|
|
407
|
+
if (title._tag === "Some") body.title = title.value;
|
|
408
|
+
if (file._tag === "Some") {
|
|
409
|
+
body.content = yield* readReportFile(file.value);
|
|
410
|
+
} else if (content._tag === "Some") {
|
|
411
|
+
body.content = content.value;
|
|
412
|
+
}
|
|
413
|
+
if (data._tag === "Some") {
|
|
414
|
+
body.data = data.value.split(",").map((d) => d.trim());
|
|
415
|
+
}
|
|
416
|
+
if (Object.keys(body).length === 0) {
|
|
417
|
+
return yield* exitWithError(
|
|
418
|
+
"Nothing to update. Provide at least one of: --title, --file, --content, --data"
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const response = yield* apiPut(
|
|
337
422
|
session.baseUrl,
|
|
338
|
-
`/milk/artifact/${session.sessionId}`,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
tags: tags.split(",").map((t) => t.trim()),
|
|
342
|
-
data: data.split(",").map((d) => d.trim())
|
|
343
|
-
}
|
|
344
|
-
).pipe(handleApiError("Failed to save artifact"));
|
|
423
|
+
`/milk/artifact/${session.sessionId}/${artifactId}`,
|
|
424
|
+
body
|
|
425
|
+
).pipe(handleApiError("Failed to update artifact"));
|
|
345
426
|
yield* Console2.log(response);
|
|
346
427
|
})
|
|
347
|
-
).pipe(Command.withDescription("
|
|
428
|
+
).pipe(Command.withDescription("Update an existing artifact"));
|
|
348
429
|
var artifactGetId = Args.text({ name: "artifactId" }).pipe(
|
|
349
430
|
Args.withDescription("Artifact ID")
|
|
350
431
|
);
|
|
@@ -360,6 +441,21 @@ var artifactGetCommand = Command.make(
|
|
|
360
441
|
yield* Console2.log(response);
|
|
361
442
|
})
|
|
362
443
|
).pipe(Command.withDescription("Retrieve a saved artifact"));
|
|
444
|
+
var artifactRenderId = Args.text({ name: "artifactId" }).pipe(
|
|
445
|
+
Args.withDescription("Artifact ID to render")
|
|
446
|
+
);
|
|
447
|
+
var artifactRenderCommand = Command.make(
|
|
448
|
+
"render",
|
|
449
|
+
{ artifactId: artifactRenderId },
|
|
450
|
+
({ artifactId }) => Effect3.gen(function* () {
|
|
451
|
+
const session = yield* requireSession;
|
|
452
|
+
const response = yield* apiGet(
|
|
453
|
+
session.baseUrl,
|
|
454
|
+
`/milk/artifact/${session.sessionId}/${artifactId}/render`
|
|
455
|
+
).pipe(handleApiError("Failed to render artifact"));
|
|
456
|
+
yield* Console2.log(response);
|
|
457
|
+
})
|
|
458
|
+
).pipe(Command.withDescription("Render an artifact (assembles reports from referenced artifacts)"));
|
|
363
459
|
var artifactListType = Options.text("type").pipe(
|
|
364
460
|
Options.withDescription("Filter by artifact type"),
|
|
365
461
|
Options.optional
|
|
@@ -385,8 +481,84 @@ var artifactListCommand = Command.make(
|
|
|
385
481
|
})
|
|
386
482
|
).pipe(Command.withDescription("List saved artifacts"));
|
|
387
483
|
var artifactCommand = Command.make("artifact").pipe(
|
|
388
|
-
Command.withDescription("Save and retrieve research artifacts"),
|
|
389
|
-
Command.withSubcommands([artifactSaveCommand, artifactGetCommand, artifactListCommand])
|
|
484
|
+
Command.withDescription("Save, update, and retrieve research artifacts"),
|
|
485
|
+
Command.withSubcommands([artifactSaveCommand, artifactUpdateCommand, artifactGetCommand, artifactRenderCommand, artifactListCommand])
|
|
486
|
+
);
|
|
487
|
+
var planSaveFile = Options.text("file").pipe(
|
|
488
|
+
Options.withAlias("f"),
|
|
489
|
+
Options.withDescription("Path to an XML file containing the plan")
|
|
490
|
+
);
|
|
491
|
+
var planSaveCommand = Command.make(
|
|
492
|
+
"save",
|
|
493
|
+
{ file: planSaveFile },
|
|
494
|
+
({ file }) => Effect3.gen(function* () {
|
|
495
|
+
const session = yield* requireSession;
|
|
496
|
+
const xmlContent = yield* readReportFile(file);
|
|
497
|
+
const response = yield* apiPost(
|
|
498
|
+
session.baseUrl,
|
|
499
|
+
`/milk/plan/${session.sessionId}/save`,
|
|
500
|
+
{ xml: xmlContent }
|
|
501
|
+
).pipe(handleApiError("Failed to save plan"));
|
|
502
|
+
yield* Console2.log(response);
|
|
503
|
+
})
|
|
504
|
+
).pipe(Command.withDescription("Save a plan from an XML file"));
|
|
505
|
+
var planStatusCommand = Command.make(
|
|
506
|
+
"status",
|
|
507
|
+
{},
|
|
508
|
+
() => Effect3.gen(function* () {
|
|
509
|
+
const session = yield* requireSession;
|
|
510
|
+
const response = yield* apiPost(
|
|
511
|
+
session.baseUrl,
|
|
512
|
+
`/milk/plan/${session.sessionId}/status`,
|
|
513
|
+
{}
|
|
514
|
+
).pipe(handleApiError("Failed to get plan status"));
|
|
515
|
+
yield* Console2.log(response);
|
|
516
|
+
})
|
|
517
|
+
).pipe(Command.withDescription("View current plan status"));
|
|
518
|
+
var planCheckStepId = Args.text({ name: "stepId" }).pipe(
|
|
519
|
+
Args.withDescription("Step ID to mark as done (e.g., broad-discovery)")
|
|
520
|
+
);
|
|
521
|
+
var planCheckCommand = Command.make(
|
|
522
|
+
"check",
|
|
523
|
+
{ stepId: planCheckStepId },
|
|
524
|
+
({ stepId }) => Effect3.gen(function* () {
|
|
525
|
+
const session = yield* requireSession;
|
|
526
|
+
const response = yield* apiPost(
|
|
527
|
+
session.baseUrl,
|
|
528
|
+
`/milk/plan/${session.sessionId}/check/${stepId}`,
|
|
529
|
+
{}
|
|
530
|
+
).pipe(handleApiError("Failed to check step"));
|
|
531
|
+
yield* Console2.log(response);
|
|
532
|
+
})
|
|
533
|
+
).pipe(Command.withDescription("Mark a plan step as done"));
|
|
534
|
+
var planSkipStepId = Args.text({ name: "stepId" }).pipe(
|
|
535
|
+
Args.withDescription("Step ID to skip (e.g., validate-gaps)")
|
|
536
|
+
);
|
|
537
|
+
var planSkipReason = Options.text("reason").pipe(
|
|
538
|
+
Options.withAlias("r"),
|
|
539
|
+
Options.withDescription("Reason for skipping this step"),
|
|
540
|
+
Options.optional
|
|
541
|
+
);
|
|
542
|
+
var planSkipCommand = Command.make(
|
|
543
|
+
"skip",
|
|
544
|
+
{ stepId: planSkipStepId, reason: planSkipReason },
|
|
545
|
+
({ stepId, reason }) => Effect3.gen(function* () {
|
|
546
|
+
const session = yield* requireSession;
|
|
547
|
+
const body = {};
|
|
548
|
+
if (reason._tag === "Some") {
|
|
549
|
+
body.reason = reason.value;
|
|
550
|
+
}
|
|
551
|
+
const response = yield* apiPost(
|
|
552
|
+
session.baseUrl,
|
|
553
|
+
`/milk/plan/${session.sessionId}/skip/${stepId}`,
|
|
554
|
+
body
|
|
555
|
+
).pipe(handleApiError("Failed to skip step"));
|
|
556
|
+
yield* Console2.log(response);
|
|
557
|
+
})
|
|
558
|
+
).pipe(Command.withDescription("Skip a plan step with an optional reason"));
|
|
559
|
+
var planCommand = Command.make("plan").pipe(
|
|
560
|
+
Command.withDescription("Save and manage research plans"),
|
|
561
|
+
Command.withSubcommands([planSaveCommand, planStatusCommand, planCheckCommand, planSkipCommand])
|
|
390
562
|
);
|
|
391
563
|
var logoutCommand = Command.make(
|
|
392
564
|
"logout",
|
|
@@ -405,13 +577,13 @@ var root = Command.make("alphamilk").pipe(
|
|
|
405
577
|
playbooksCommand,
|
|
406
578
|
playbookCommand,
|
|
407
579
|
probeCommand,
|
|
408
|
-
|
|
409
|
-
|
|
580
|
+
artifactCommand,
|
|
581
|
+
planCommand
|
|
410
582
|
])
|
|
411
583
|
);
|
|
412
584
|
var cli = Command.run(root, {
|
|
413
585
|
name: "alphamilk",
|
|
414
|
-
version: "0.0.
|
|
586
|
+
version: "0.0.3"
|
|
415
587
|
});
|
|
416
588
|
var MainLayer = Layer.mergeAll(
|
|
417
589
|
NodeContext.layer,
|