@keystrokehq/cli 0.1.38 → 0.2.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/dist/{dist-DkLbeW8l.mjs → dist-BOhrc_Nv.mjs} +198 -561
- package/dist/dist-BOhrc_Nv.mjs.map +1 -0
- package/dist/{dist-B6z1wti6.mjs → dist-D-cLLjHv.mjs} +87 -2017
- package/dist/dist-D-cLLjHv.mjs.map +1 -0
- package/dist/{dist-GSI9JDuz.mjs → dist-DGKF3FGu.mjs} +31 -265
- package/dist/dist-DGKF3FGu.mjs.map +1 -0
- package/dist/dist-DMuIdus5.mjs +3 -0
- package/dist/{dist-gAvgHBlr.mjs → dist-Re6HHSqz.mjs} +2 -2
- package/dist/{dist-gAvgHBlr.mjs.map → dist-Re6HHSqz.mjs.map} +1 -1
- package/dist/index.mjs +177 -463
- package/dist/index.mjs.map +1 -1
- package/dist/{maybe-auto-update-Dv4MJvWb.mjs → maybe-auto-update-q5MthdI8.mjs} +2 -2
- package/dist/{maybe-auto-update-Dv4MJvWb.mjs.map → maybe-auto-update-q5MthdI8.mjs.map} +1 -1
- package/dist/skills-bundle/_AGENTS.mcp.md +5 -9
- package/dist/skills-bundle/_AGENTS.md +112 -243
- package/dist/skills-bundle/skills/keystroke-actions/SKILL.md +160 -0
- package/dist/skills-bundle/skills/keystroke-actions/references/catalog-and-imports.md +71 -0
- package/dist/skills-bundle/skills/keystroke-agents/SKILL.md +115 -0
- package/dist/skills-bundle/skills/keystroke-agents/references/models.md +23 -0
- package/dist/skills-bundle/skills/keystroke-agents/references/tools-mcp-codemode.md +73 -0
- package/dist/skills-bundle/skills/keystroke-agents/references/workflows-and-testing.md +26 -0
- package/dist/skills-bundle/skills/keystroke-apps/SKILL.md +151 -0
- package/dist/skills-bundle/skills/keystroke-apps/references/cli-and-catalog.md +104 -0
- package/dist/skills-bundle/skills/keystroke-channels/SKILL.md +66 -0
- package/dist/skills-bundle/skills/keystroke-channels/references/slack-setup.md +41 -0
- package/dist/skills-bundle/skills/keystroke-cli/SKILL.md +93 -0
- package/dist/skills-bundle/skills/keystroke-deploy/SKILL.md +93 -0
- package/dist/skills-bundle/skills/keystroke-deploy/references/build-and-full-deploy.md +30 -0
- package/dist/skills-bundle/skills/keystroke-deploy/references/filtered-deploy.md +50 -0
- package/dist/skills-bundle/skills/keystroke-deploy/references/wip-ignore.md +35 -0
- package/dist/skills-bundle/skills/keystroke-files/SKILL.md +43 -0
- package/dist/skills-bundle/skills/keystroke-skills/SKILL.md +42 -0
- package/dist/skills-bundle/skills/keystroke-triggers/SKILL.md +143 -0
- package/dist/skills-bundle/skills/keystroke-workflows/SKILL.md +78 -0
- package/dist/skills-bundle/skills/keystroke-workflows/references/authoring.md +168 -0
- package/dist/skills-bundle/skills/keystroke-workflows/references/testing.md +138 -0
- package/dist/templates/hello-world/.env.example +4 -0
- package/dist/templates/hello-world/README.md +3 -4
- package/dist/templates/hello-world/package.json +0 -1
- package/dist/templates/hello-world/src/actions/greet.ts +0 -1
- package/dist/templates/hello-world/src/agents/hello.ts +0 -2
- package/dist/templates/hello-world/src/workflows/greeting.ts +0 -1
- package/dist/{version-CiFlKPyE.mjs → version-DcR3O1UD.mjs} +3 -2
- package/dist/version-DcR3O1UD.mjs.map +1 -0
- package/package.json +5 -5
- package/dist/dist-B6z1wti6.mjs.map +0 -1
- package/dist/dist-CjWXZCN7.mjs +0 -3
- package/dist/dist-DkLbeW8l.mjs.map +0 -1
- package/dist/dist-GSI9JDuz.mjs.map +0 -1
- package/dist/version-CiFlKPyE.mjs.map +0 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Workflow authoring
|
|
2
|
+
|
|
3
|
+
## Keep steps in actions (when they are not agent prompts)
|
|
4
|
+
|
|
5
|
+
Workflow `run` orchestrates — call actions for deterministic logic and integrations, call agents directly for LLM steps:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
async run(input) {
|
|
9
|
+
const { brief } = await researchSignup.run(input);
|
|
10
|
+
const summary = await support.prompt({
|
|
11
|
+
message: `Summarize signup research:\n${brief}`,
|
|
12
|
+
});
|
|
13
|
+
const sent = await slackSendMessage.run({
|
|
14
|
+
channel: "#pipeline",
|
|
15
|
+
markdown_text: signupBriefMessage({ ...input, brief }),
|
|
16
|
+
});
|
|
17
|
+
return { brief, ...sent };
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Actions are leaf units: an action never calls another action — compose them in the workflow. Need formatting or shared logic between steps? Put it in `src/lib/`, not in a wrapper action.
|
|
22
|
+
|
|
23
|
+
## Agents as workflow steps
|
|
24
|
+
|
|
25
|
+
Import the agent and call `.prompt()` directly in the workflow `run`. Each call is a durable step keyed as `step:<agent-slug>#<occurrence>` (same scheme as actions). No wrapper action required when the step is just "prompt this agent."
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const result = await support.prompt({
|
|
29
|
+
message: `From: ${input.sender}\nSubject: ${input.latestSubject}`,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use a wrapper action only when the agent call should also be an agent tool, or when the step bundles non-prompt logic.
|
|
34
|
+
|
|
35
|
+
For an explicit step id, actions chain `.stepId("x")`; agent prompts take it as an option: `agent.prompt(input, { stepId: "x" })`.
|
|
36
|
+
|
|
37
|
+
## LLM steps (`promptLlm`)
|
|
38
|
+
|
|
39
|
+
For a one-shot LLM call without a full agent, use `promptLlm` (from `@keystrokehq/keystroke/workflow`). It's a first-class durable step keyed `step:promptLlm#<occurrence>`. The signature is `promptLlm(prompt, opts)` — the prompt string comes **first**, options second:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { promptLlm } from "@keystrokehq/keystroke/workflow";
|
|
43
|
+
|
|
44
|
+
// Returns a string when no outputSchema is given:
|
|
45
|
+
const summary = await promptLlm(`Summarize:\n${brief}`, {
|
|
46
|
+
model: "anthropic/claude-sonnet-4.6",
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Pass `outputSchema` (Zod) to get a parsed, schema-validated object back instead of a string — the return type is inferred from the schema:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { z } from "zod";
|
|
54
|
+
|
|
55
|
+
const Category = z.object({ category: z.enum(["bug", "feature", "question"]) });
|
|
56
|
+
|
|
57
|
+
const { category } = await promptLlm(`Classify this ticket:\n${ticket}`, {
|
|
58
|
+
model: "anthropic/claude-sonnet-4.6",
|
|
59
|
+
outputSchema: Category,
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Other options: `system`, `thinkingLevel`, `temperature`, `maxTokens`, `stepId`.
|
|
64
|
+
|
|
65
|
+
## Sub-workflows as steps
|
|
66
|
+
|
|
67
|
+
Reuse a whole workflow by importing it and awaiting `.run(input)` — a first-class durable step, same as an action. Keystroke validates the sub-workflow's `input`/`output`, records its output as a checkpoint keyed `step:<sub-slug>#<occurrence>`, and gives it a trace span.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import enrichLead from "./enrich-lead";
|
|
71
|
+
|
|
72
|
+
async run(input) {
|
|
73
|
+
const enriched = await enrichLead.run({ email: input.email });
|
|
74
|
+
return { score: enriched.score };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The sub-workflow runs **inline** in the same run: its inner action/agent/`promptLlm` steps and durable waits (`ctx.sleep` / `ctx.hook`) all stay durable, and it does **not** spawn a separate tracked run. Never thread `ctx` — `enrichLead.run(input, ctx)` is not the pattern; the engine supplies the context. Pin an explicit id with `.stepId("x")` when the call's position can shift. For an *independently tracked* run instead, expose the workflow as an agent tool (`defineWorkflowTool`) or call it over HTTP.
|
|
79
|
+
|
|
80
|
+
## Durability & retries
|
|
81
|
+
|
|
82
|
+
As each step completes, its result is written to a run event log. If a later step fails and the run is retried, Keystroke replays the log: completed steps return their recorded result instead of running again, and execution resumes at the first unfinished step. Design for this:
|
|
83
|
+
|
|
84
|
+
- **Put side effects inside steps.** Work in the `run` body that isn't an action / agent / `promptLlm` call (a raw `fetch`, a DB write, `Date.now()`, `Math.random()`) runs again on every replay. Move it into a step so it's recorded once.
|
|
85
|
+
- **Steps should be idempotent.** A step can be retried after a transient failure, so an action should tolerate being called twice with the same input (use idempotency keys for external writes where it matters).
|
|
86
|
+
- **Keep control flow deterministic.** Branch on the run's input and recorded step results — not on wall-clock time or randomness — so replays take the same path. Each step is recorded under `step:<slug>#<occurrence>`; pin `.stepId("x")` when an action's order can shift so replays still line up. Ids must be unique within a run.
|
|
87
|
+
|
|
88
|
+
You don't manage retries yourself. The runtime records `step_completed`, `step_retrying`, and `step_failed` events and surfaces them in `keystroke workflow runs get <slug> <run-id> --include steps,trace`.
|
|
89
|
+
|
|
90
|
+
## Parallel steps
|
|
91
|
+
|
|
92
|
+
Independent steps run concurrently with `Promise.all` — each `.run()` / `.prompt()` / `promptLlm` inside it is still its own durable step, recorded and replayed individually:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const [enriched, scored] = await Promise.all([
|
|
96
|
+
enrichLead.run({ leadId }),
|
|
97
|
+
scoreLead.run({ leadId }),
|
|
98
|
+
]);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Correlation ids are assigned per slug in array order (`step:enrich-lead#0`, `step:score-lead#0`). If the branches can change order between attempts (conditionally included, dynamic arrays), pin a stable `.stepId()` on each so replays line up:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const results = await Promise.all(
|
|
105
|
+
leads.map((lead) => enrichLead.run({ leadId: lead.id }).stepId(`enrich:${lead.id}`)),
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
There's no built-in concurrency limit or fan-out helper — `Promise.all` runs everything at once. For bounded concurrency, batch the array yourself (e.g. chunk and `await` each chunk).
|
|
110
|
+
|
|
111
|
+
## Handling failures
|
|
112
|
+
|
|
113
|
+
Retries are **queue-level**: a failed run is re-enqueued (default 3 attempts, exponential backoff) and replays from the event log, so completed steps don't re-run. Each failed attempt emits `step_retrying`; the final failure emits `step_failed`. You don't write retry loops — make steps idempotent so a retried step is safe.
|
|
114
|
+
|
|
115
|
+
There's no built-in saga, compensation engine, or dead-letter queue. When a later step fails and you need to undo earlier work, orchestrate it yourself with `try/catch` in `run` — catching the error lets the run complete normally (so attach a `compensated` flag to the output rather than rethrowing if you've handled it):
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
async run(input) {
|
|
119
|
+
await reserveBooking.run({ id: input.bookingId });
|
|
120
|
+
try {
|
|
121
|
+
await chargeCustomer.run({ id: input.bookingId });
|
|
122
|
+
return { ok: true };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
await releaseBooking.run({ id: input.bookingId }); // best-effort cleanup
|
|
125
|
+
return { ok: false, compensated: true };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If you let the error propagate instead, the run fails and the queue retries it — fine for transient failures, but it replays from the log, so any cleanup must itself be a recorded, idempotent step.
|
|
131
|
+
|
|
132
|
+
## Durable run context (`ctx`)
|
|
133
|
+
|
|
134
|
+
`run(input, ctx)` receives a second argument exposing `runId`, `trigger` (`api` | `cron` | `webhook` | `poll` | `retry`), and the durable wait primitives `ctx.sleep(...)` and `ctx.hook(...)`. Both **suspend** the run (the replay engine resumes it later) so long waits and external callbacks don't hold a process open.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// Durable delay: number (ms), duration string, or a Date
|
|
138
|
+
await ctx.sleep("1h");
|
|
139
|
+
|
|
140
|
+
// Durable hook: suspend until an external system POSTs to the resume URL
|
|
141
|
+
async run(input, ctx) {
|
|
142
|
+
const approval = ctx.hook<{ approved: boolean }>();
|
|
143
|
+
await slackSendMessage.run({
|
|
144
|
+
channel: "#approvals",
|
|
145
|
+
markdown_text: `Approve deploy? ${approval.resumeUrl}`,
|
|
146
|
+
});
|
|
147
|
+
const { approved } = await approval; // suspends here until resumed
|
|
148
|
+
return { approved };
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`ctx.hook(options?)` takes an optional `token` (reuse a stable resume token) and `schema` (Zod validation of the resume payload). The returned handle exposes `token` and `resumeUrl` before you await it — surface that URL so the approver/callback can resume the run.
|
|
153
|
+
|
|
154
|
+
## Agents inside actions
|
|
155
|
+
|
|
156
|
+
When an action must call an agent (e.g. the action is reused as an agent tool), call `.prompt()` inside the action's `run`. That prompt is not a separate workflow step — the action is.
|
|
157
|
+
|
|
158
|
+
## Connecting apps
|
|
159
|
+
|
|
160
|
+
Sync the app into `src/apps/`, author with `app.action()`, then `keystroke connect <slug>` before testing. Catalog integration actions from npm packages work once the app is connected.
|
|
161
|
+
|
|
162
|
+
## Slugs
|
|
163
|
+
|
|
164
|
+
Slugs are scoped per primitive kind: a `slug` must be unique among workflows, among agents, and among triggers, but the same `slug` may be reused across kinds (a `pricing` agent and a `pricing` workflow can coexist). The workflow's `slug` is also its HTTP route segment (`POST /workflows/{slug}`).
|
|
165
|
+
|
|
166
|
+
## Subscription mode
|
|
167
|
+
|
|
168
|
+
`defineWorkflow` accepts an optional `subscription: { mode: "system" | "subscribable" }` to control how the workflow can be subscribed to. Omit it for the default behavior.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Testing & debugging workflows
|
|
2
|
+
|
|
3
|
+
## Unit tests (in-process, no server)
|
|
4
|
+
|
|
5
|
+
Test through `executeWorkflow` from `@keystrokehq/keystroke/workflow` — it parses input/output and wires up the action/sub-workflow runners. **Never call `workflow.run(...)` directly at the top level of a test**: outside a run it has no durable context (`workflow.run(input)` returns an unresolvable step; `workflow.run(input, ctx)` skips the checkpoint boundary). Inside another workflow body, `await otherWorkflow.run(input)` *is* the correct first-class sub-workflow step.
|
|
6
|
+
|
|
7
|
+
`executeWorkflow` runs the workflow via the durable replay engine and resolves to a `ReplayResult`: `{ status: "completed", output }`, `{ status: "failed", error }`, `{ status: "suspended", items }` (when the body hits `ctx.sleep`/`ctx.hook`), or `{ status: "canceled" }`. Assert on `status` and read `output`.
|
|
8
|
+
|
|
9
|
+
`keystroke init` scaffolds Vitest and a starter test (e.g. `src/workflows/greeting.test.ts`):
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { executeWorkflow } from "@keystrokehq/keystroke/workflow";
|
|
14
|
+
import greeting from "./greeting";
|
|
15
|
+
|
|
16
|
+
describe("greeting workflow", () => {
|
|
17
|
+
it("returns a greeting for a name", async () => {
|
|
18
|
+
const result = await executeWorkflow(greeting, { name: "Ada" });
|
|
19
|
+
expect(result).toEqual({ status: "completed", output: { greeting: "Hello, Ada!" } });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use `pnpm test:unit` for `src/**/*.test.ts`. Integration tests (`*.int.test.ts`) load `.env` and skip when required keys are unset (`pnpm test:int`).
|
|
25
|
+
|
|
26
|
+
For multi-step workflows:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { describe, expect, it } from "vitest";
|
|
30
|
+
import { executeWorkflow } from "@keystrokehq/keystroke/workflow";
|
|
31
|
+
import workflow from "../signup-pipeline";
|
|
32
|
+
|
|
33
|
+
it("runs the pipeline", async () => {
|
|
34
|
+
const result = await executeWorkflow(workflow, {
|
|
35
|
+
name: "Ada",
|
|
36
|
+
email: "ada@acme.com",
|
|
37
|
+
company: "Acme",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.status).toBe("completed");
|
|
41
|
+
if (result.status === "completed") {
|
|
42
|
+
expect(result.output).toMatchObject({ channel: "#pipeline" });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Deterministic actions (pure logic, no network/LLM) need no mocks — run the workflow and assert.
|
|
48
|
+
|
|
49
|
+
## Stub expensive actions by seeding the event log
|
|
50
|
+
|
|
51
|
+
Steps are checkpointed in the durable event log as `step_completed` events keyed by a stable **correlation id**. Before running a step the action runner checks the log for that correlation id; pre-seed a `step_completed` event to skip real work (LLM/agent/HTTP calls) and feed a fixture:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { executeWorkflow, MemoryEventLog } from "@keystrokehq/keystroke/workflow";
|
|
55
|
+
import workflow from "../signup-pipeline";
|
|
56
|
+
|
|
57
|
+
it("drafts without calling the agent", async () => {
|
|
58
|
+
const eventLog = new MemoryEventLog();
|
|
59
|
+
const runId = "test-run";
|
|
60
|
+
|
|
61
|
+
// correlation id = `step:<actionKey>#<occurrence>` (occurrence starts at 0),
|
|
62
|
+
// or `step:<stepId>` if the step used `.stepId("...")`.
|
|
63
|
+
await eventLog.append({
|
|
64
|
+
runId,
|
|
65
|
+
type: "step_completed",
|
|
66
|
+
correlationId: "step:research-signup#0",
|
|
67
|
+
data: { brief: "stubbed brief" }, // the action output goes in `data` directly
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await executeWorkflow(
|
|
71
|
+
workflow,
|
|
72
|
+
{ name: "Ada", email: "ada@acme.com", company: "Acme" },
|
|
73
|
+
{ runId, eventLog },
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result.status).toBe("completed");
|
|
77
|
+
if (result.status === "completed") {
|
|
78
|
+
expect(result.output).toMatchObject({ brief: "stubbed brief" });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Rules:
|
|
84
|
+
|
|
85
|
+
- Pass a fixed `runId` and the same `MemoryEventLog` to `executeWorkflow`.
|
|
86
|
+
- The correlation id is `step:<slug>#<occurrence>` — `#0` for the first call to that action/agent, `#1` for the second, and so on — unless an explicit step id was set (`action.stepId("x")` or `agent.prompt(input, { stepId: "x" })`), in which case it is `step:x`.
|
|
87
|
+
- The action **output** is stored as the event `data` directly (no wrapper). It is re-validated against the action's output schema, so it must be schema-valid.
|
|
88
|
+
|
|
89
|
+
This is the preferred mock: schema-checked, no module-mock hoisting, and it still exercises the real `run` orchestration (branching, loops, output shape) — only the action bodies are stubbed.
|
|
90
|
+
|
|
91
|
+
## Mock the integration instead (alternative)
|
|
92
|
+
|
|
93
|
+
When you'd rather stub the underlying dependency (e.g. the agent inside an action) or assert how it was called, use `vi.mock`:
|
|
94
|
+
|
|
95
|
+
`agent.prompt()` resolves to a `PromptResponse` — `{ sessionId, messages, error, canceled?, output? }` (`output` is the parsed result when the prompt was called with an `outputSchema`), **not** a bare messages array. Mock that shape:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { vi } from "vitest";
|
|
99
|
+
|
|
100
|
+
vi.mock("../agents/support", () => ({
|
|
101
|
+
default: {
|
|
102
|
+
prompt: vi.fn().mockResolvedValue({
|
|
103
|
+
sessionId: "test-session",
|
|
104
|
+
messages: [{ role: "assistant", content: "mocked" }],
|
|
105
|
+
error: null,
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Assert the input/output contract
|
|
112
|
+
|
|
113
|
+
Validation happens at the `executeWorkflow` boundary, so contract tests are free:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
await expect(executeWorkflow(workflow, { email: "x" } as never)).rejects.toThrow();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Debug runs from the CLI
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
keystroke workflow run signup-pipeline --input '{"name":"Ada","email":"ada@acme.com","company":"Acme"}'
|
|
123
|
+
keystroke workflow runs list signup-pipeline
|
|
124
|
+
keystroke workflow runs get signup-pipeline <run-id> --include steps,trace
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`steps` shows which actions ran and failed. `trace` links nested agent calls.
|
|
128
|
+
|
|
129
|
+
### Trigger-fired runs
|
|
130
|
+
|
|
131
|
+
When a cron/webhook/poll fired the workflow:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
keystroke trigger runs list signup:signup-pipeline
|
|
135
|
+
keystroke trigger runs get signup:signup-pipeline <run-id> --include workflows,trace
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
While developing triggers, prefer `keystroke workflow run` until the workflow logic is solid.
|
|
@@ -9,8 +9,7 @@ Keystroke is deploy-first: build in `src/`, deploy to your platform project, the
|
|
|
9
9
|
```bash
|
|
10
10
|
pnpm install # @keystrokehq/* from GitHub Packages (see .npmrc)
|
|
11
11
|
keystroke auth login # once
|
|
12
|
-
keystroke
|
|
13
|
-
keystroke deploy # build + ship src/
|
|
12
|
+
keystroke deploy --project <id> # build + ship src/ (see: keystroke project list)
|
|
14
13
|
```
|
|
15
14
|
|
|
16
15
|
<!-- example:start -->
|
|
@@ -26,7 +25,7 @@ Use `--filter agents/hello` (or `workflows/greeting`) to redeploy a single modul
|
|
|
26
25
|
|
|
27
26
|
## Run locally (optional)
|
|
28
27
|
|
|
29
|
-
To iterate offline without deploying, run a local server.
|
|
28
|
+
To iterate offline without deploying, run a local server. Set `ANTHROPIC_API_KEY` and any integration keys in `.env` (created from `.env.example` on init), then:
|
|
30
29
|
|
|
31
30
|
```bash
|
|
32
31
|
keystroke dev # watch src/, rebuild, restart the API (API :3002, dashboard :3000)
|
|
@@ -63,5 +62,5 @@ src/
|
|
|
63
62
|
skills/ # agent skills (SKILL.md)
|
|
64
63
|
triggers/ # cron, webhook, poll attachments
|
|
65
64
|
workflows/ # defineWorkflow
|
|
66
|
-
keystroke.config.ts # optional server
|
|
65
|
+
keystroke.config.ts # optional server + custom integration config
|
|
67
66
|
```
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { defineAgent } from "@keystrokehq/keystroke/agent";
|
|
2
2
|
|
|
3
3
|
export default defineAgent({
|
|
4
|
-
name: "Hello",
|
|
5
|
-
description: "Hello description.",
|
|
6
4
|
slug: "hello",
|
|
7
5
|
systemPrompt: "You are a friendly hello-world agent. Keep replies short.",
|
|
8
6
|
model: "anthropic/claude-sonnet-4.6",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { Kn as originFromPublicUrl, ut as LOCAL_PLATFORM_ORIGIN } from "./dist-BOhrc_Nv.mjs";
|
|
3
3
|
import Conf from "conf";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
@@ -58,6 +58,7 @@ function createCliConfig(cwd = getCliConfigDir()) {
|
|
|
58
58
|
default: DEFAULT_PLATFORM_URL
|
|
59
59
|
},
|
|
60
60
|
activeOrganizationId: { type: "string" },
|
|
61
|
+
activeProjectId: { type: "string" },
|
|
61
62
|
apiTarget: {
|
|
62
63
|
type: "string",
|
|
63
64
|
enum: ["local", "platform"]
|
|
@@ -165,4 +166,4 @@ function readCliVersion() {
|
|
|
165
166
|
//#endregion
|
|
166
167
|
export { installPlaygroundDependencies as a, createCliConfig as c, getEffectiveApiTarget as d, getPlatformUrl as f, resolvePlatformUrlForWebUrl as g, DEFAULT_WEB_URL as h, installDependencies as i, getCliConfigDir as l, DEFAULT_PLATFORM_URL as m, buildPlaygroundWorkspace as n, resolvePackageManager as o, getWebUrl as p, detectPackageManager as r, resolveCliRoot as s, readCliVersion as t, getConfigDir as u };
|
|
167
168
|
|
|
168
|
-
//# sourceMappingURL=version-
|
|
169
|
+
//# sourceMappingURL=version-DcR3O1UD.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-DcR3O1UD.mjs","names":[],"sources":["../src/resolve-platform-url.ts","../src/config.ts","../src/project/resolve-cli-root.ts","../src/init/package-manager.ts","../src/version.ts"],"sourcesContent":["import { LOCAL_PLATFORM_ORIGIN, originFromPublicUrl } from \"@keystrokehq/shared\";\n\nexport const DEFAULT_WEB_URL = \"https://keystroke.ai\";\nexport const DEFAULT_PLATFORM_URL = \"https://api.keystroke.ai\";\nexport const LOCAL_WEB_URL = \"http://localhost:3000\";\n\nexport type ResolvePlatformUrlOptions = {\n platformUrl?: string;\n /** Used when webUrl does not match a known deployment. */\n fallback?: string;\n};\n\nfunction webOriginFromUrl(webUrl: string): string {\n return originFromPublicUrl(webUrl, webUrl.replace(/\\/+$/, \"\"));\n}\n\n/** Known web origins with a fixed platform API — undefined for custom/staging URLs. */\nexport function knownPlatformUrlForWebUrl(webUrl: string): string | undefined {\n const webOrigin = webOriginFromUrl(webUrl);\n\n if (webOrigin === LOCAL_WEB_URL || webOrigin === \"http://127.0.0.1:3000\") {\n return LOCAL_PLATFORM_ORIGIN;\n }\n\n if (webOrigin === DEFAULT_WEB_URL) {\n return DEFAULT_PLATFORM_URL;\n }\n\n return undefined;\n}\n\nexport function resolvePlatformUrlForWebUrl(\n webUrl: string,\n options: ResolvePlatformUrlOptions = {},\n): string {\n const explicit = options.platformUrl?.trim();\n if (explicit) {\n return explicit.replace(/\\/+$/, \"\");\n }\n\n const known = knownPlatformUrlForWebUrl(webUrl);\n if (known) {\n return known;\n }\n\n const fallback = options.fallback?.trim() || DEFAULT_PLATFORM_URL;\n return fallback.replace(/\\/+$/, \"\");\n}\n","import Conf from \"conf\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport {\n DEFAULT_PLATFORM_URL,\n DEFAULT_WEB_URL,\n knownPlatformUrlForWebUrl,\n resolvePlatformUrlForWebUrl,\n} from \"./resolve-platform-url\";\n\nexport type ApiTargetMode = \"local\" | \"platform\";\n\nexport type CliConfig = {\n webUrl: string;\n platformUrl: string;\n activeOrganizationId?: string;\n activeProjectId?: string;\n apiTarget?: ApiTargetMode;\n};\n\nexport function getCliConfigDir(cwd = join(homedir(), \".keystroke\")): string {\n return cwd;\n}\n\nexport function getConfigDir(config: Conf<CliConfig>): string {\n return dirname(config.path);\n}\n\nexport function getEffectiveApiTarget(config: Conf<CliConfig>): ApiTargetMode {\n const explicit = config.get(\"apiTarget\");\n if (explicit === \"local\" || explicit === \"platform\") {\n return explicit;\n }\n\n if (process.env.KEYSTROKE_API_KEY?.trim()) {\n return \"platform\";\n }\n\n return \"local\";\n}\n\nfunction syncPlatformUrlWithWebUrl(config: Conf<CliConfig>): void {\n const webUrl = config.get(\"webUrl\");\n const known = knownPlatformUrlForWebUrl(webUrl);\n if (!known) {\n return;\n }\n\n const stored = config.get(\"platformUrl\");\n if (stored !== known) {\n config.set(\"platformUrl\", known);\n }\n}\n\nexport function createCliConfig(cwd = getCliConfigDir()): Conf<CliConfig> {\n const config = new Conf<CliConfig>({\n projectName: \"keystroke\",\n cwd,\n schema: {\n webUrl: {\n type: \"string\",\n default: DEFAULT_WEB_URL,\n },\n platformUrl: {\n type: \"string\",\n default: DEFAULT_PLATFORM_URL,\n },\n activeOrganizationId: {\n type: \"string\",\n },\n activeProjectId: {\n type: \"string\",\n },\n apiTarget: {\n type: \"string\",\n enum: [\"local\", \"platform\"],\n },\n },\n });\n\n syncPlatformUrlWithWebUrl(config);\n return config;\n}\n\nexport function getWebUrl(config: Conf<CliConfig>): string {\n return config.get(\"webUrl\");\n}\n\nexport function getPlatformUrl(config: Conf<CliConfig>): string {\n const apiKey = process.env.KEYSTROKE_API_KEY?.trim();\n const fromEnv = process.env.KEYSTROKE_PLATFORM_URL?.trim();\n if (apiKey && fromEnv) {\n return fromEnv.replace(/\\/+$/, \"\");\n }\n return resolvePlatformUrlForWebUrl(getWebUrl(config), {\n fallback: config.get(\"platformUrl\"),\n });\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction isCliPackage(dir: string): boolean {\n const packageJsonPath = join(dir, \"package.json\");\n if (!existsSync(packageJsonPath)) {\n return false;\n }\n\n const pkg = JSON.parse(readFileSync(packageJsonPath, \"utf8\")) as { name?: string };\n return pkg.name === \"@keystrokehq/cli\";\n}\n\nexport function resolveCliRoot(fromModuleUrl: string): string {\n let dir = dirname(fileURLToPath(fromModuleUrl));\n\n while (dir !== dirname(dir)) {\n if (isCliPackage(dir)) {\n return dir;\n }\n\n dir = dirname(dir);\n }\n\n throw new Error(\"Could not resolve keystroke CLI package root\");\n}\n","import { spawnSync } from \"node:child_process\";\n\nexport type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\nconst MANAGERS: PackageManager[] = [\"pnpm\", \"npm\", \"yarn\", \"bun\"];\n\nexport function detectPackageManager(): PackageManager {\n const userAgent = process.env.npm_config_user_agent ?? \"\";\n\n for (const manager of MANAGERS) {\n if (userAgent.startsWith(manager)) {\n return manager;\n }\n }\n\n if (process.env.PNPM_HOME) {\n return \"pnpm\";\n }\n\n return \"npm\";\n}\n\nexport function resolvePackageManager(explicit?: string): PackageManager {\n if (!explicit) {\n return detectPackageManager();\n }\n\n const normalized = explicit.trim().toLowerCase();\n if (!MANAGERS.includes(normalized as PackageManager)) {\n throw new Error(`Unsupported package manager \"${explicit}\". Use npm, pnpm, yarn, or bun.`);\n }\n\n return normalized as PackageManager;\n}\n\n/** Resolve GitHub Packages auth for @keystrokehq/* when NODE_AUTH_TOKEN is unset. */\nexport function resolveGithubPackagesToken(): string | undefined {\n if (process.env.NODE_AUTH_TOKEN) {\n return process.env.NODE_AUTH_TOKEN;\n }\n\n for (const key of [\"GITHUB_TOKEN\", \"GH_TOKEN\"] as const) {\n const value = process.env[key];\n if (value) {\n return value;\n }\n }\n\n const gh = spawnSync(\"gh\", [\"auth\", \"token\"], { encoding: \"utf8\" });\n if (gh.status === 0) {\n const token = gh.stdout.trim();\n if (token) {\n return token;\n }\n }\n\n return undefined;\n}\n\nexport function installDependencies(cwd: string, manager: PackageManager): void {\n const token = resolveGithubPackagesToken();\n const env = token ? { ...process.env, NODE_AUTH_TOKEN: token } : process.env;\n\n const result = spawnSync(manager, [\"install\"], {\n cwd,\n stdio: \"inherit\",\n env,\n });\n\n if (result.status !== 0) {\n throw new Error(`${manager} install failed`);\n }\n}\n\nexport function installPlaygroundDependencies(cwd: string): void {\n const result = spawnSync(\"pnpm\", [\"install\", \"--ignore-workspace\"], {\n cwd,\n stdio: \"inherit\",\n env: process.env,\n });\n\n if (result.status !== 0) {\n throw new Error(\"pnpm install failed\");\n }\n}\n\n/**\n * Build every workspace package so the playground's `link:` deps resolve to\n * compiled `dist/`. Without this, `keystroke dev` fails with\n * `ERR_MODULE_NOT_FOUND` against whichever package was never built. Turbo\n * caches, so this is only slow on the first run.\n */\nexport function buildPlaygroundWorkspace(monorepoRoot: string): void {\n // Build with a clean Node env: the `keystroke-dev` wrapper sets\n // NODE_OPTIONS=--conditions=development, which would make tsdown/turbo resolve\n // their own deps to src/ and fail. Dropping NODE_OPTIONS builds to dist/ normally.\n const { NODE_OPTIONS: _drop, ...buildEnv } = process.env;\n const result = spawnSync(\"pnpm\", [\"run\", \"build\"], {\n cwd: monorepoRoot,\n stdio: \"inherit\",\n env: buildEnv,\n });\n\n if (result.status !== 0) {\n throw new Error(\"workspace build failed (pnpm run build)\");\n }\n}\n","import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { resolveCliRoot } from \"./project/resolve-cli-root\";\n\nexport function readCliVersion(): string {\n const packageJsonPath = join(resolveCliRoot(import.meta.url), \"package.json\");\n const pkg = JSON.parse(readFileSync(packageJsonPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n}\n"],"mappings":";;;;;;;;;AAEA,MAAa,kBAAkB;AAC/B,MAAa,uBAAuB;AASpC,SAAS,iBAAiB,QAAwB;CAChD,OAAO,oBAAoB,QAAQ,OAAO,QAAQ,QAAQ,EAAE,CAAC;AAC/D;;AAGA,SAAgB,0BAA0B,QAAoC;CAC5E,MAAM,YAAY,iBAAiB,MAAM;CAEzC,IAAI,cAAA,2BAA+B,cAAc,yBAC/C,OAAO;CAGT,IAAI,cAAA,wBACF,OAAO;AAIX;AAEA,SAAgB,4BACd,QACA,UAAqC,CAAC,GAC9B;CACR,MAAM,WAAW,QAAQ,aAAa,KAAK;CAC3C,IAAI,UACF,OAAO,SAAS,QAAQ,QAAQ,EAAE;CAGpC,MAAM,QAAQ,0BAA0B,MAAM;CAC9C,IAAI,OACF,OAAO;CAIT,QADiB,QAAQ,UAAU,KAAK,KAAA,4BACxB,QAAQ,QAAQ,EAAE;AACpC;;;AC1BA,SAAgB,gBAAgB,MAAM,KAAK,QAAQ,GAAG,YAAY,GAAW;CAC3E,OAAO;AACT;AAEA,SAAgB,aAAa,QAAiC;CAC5D,OAAO,QAAQ,OAAO,IAAI;AAC5B;AAEA,SAAgB,sBAAsB,QAAwC;CAC5E,MAAM,WAAW,OAAO,IAAI,WAAW;CACvC,IAAI,aAAa,WAAW,aAAa,YACvC,OAAO;CAGT,IAAI,QAAQ,IAAI,mBAAmB,KAAK,GACtC,OAAO;CAGT,OAAO;AACT;AAEA,SAAS,0BAA0B,QAA+B;CAEhE,MAAM,QAAQ,0BADC,OAAO,IAAI,QACmB,CAAC;CAC9C,IAAI,CAAC,OACH;CAIF,IADe,OAAO,IAAI,aACjB,MAAM,OACb,OAAO,IAAI,eAAe,KAAK;AAEnC;AAEA,SAAgB,gBAAgB,MAAM,gBAAgB,GAAoB;CACxE,MAAM,SAAS,IAAI,KAAgB;EACjC,aAAa;EACb;EACA,QAAQ;GACN,QAAQ;IACN,MAAM;IACN,SAAS;GACX;GACA,aAAa;IACX,MAAM;IACN,SAAS;GACX;GACA,sBAAsB,EACpB,MAAM,SACR;GACA,iBAAiB,EACf,MAAM,SACR;GACA,WAAW;IACT,MAAM;IACN,MAAM,CAAC,SAAS,UAAU;GAC5B;EACF;CACF,CAAC;CAED,0BAA0B,MAAM;CAChC,OAAO;AACT;AAEA,SAAgB,UAAU,QAAiC;CACzD,OAAO,OAAO,IAAI,QAAQ;AAC5B;AAEA,SAAgB,eAAe,QAAiC;CAC9D,MAAM,SAAS,QAAQ,IAAI,mBAAmB,KAAK;CACnD,MAAM,UAAU,QAAQ,IAAI,wBAAwB,KAAK;CACzD,IAAI,UAAU,SACZ,OAAO,QAAQ,QAAQ,QAAQ,EAAE;CAEnC,OAAO,4BAA4B,UAAU,MAAM,GAAG,EACpD,UAAU,OAAO,IAAI,aAAa,EACpC,CAAC;AACH;;;AC9FA,SAAS,aAAa,KAAsB;CAC1C,MAAM,kBAAkB,KAAK,KAAK,cAAc;CAChD,IAAI,CAAC,WAAW,eAAe,GAC7B,OAAO;CAIT,OADY,KAAK,MAAM,aAAa,iBAAiB,MAAM,CAClD,EAAE,SAAS;AACtB;AAEA,SAAgB,eAAe,eAA+B;CAC5D,IAAI,MAAM,QAAQ,cAAc,aAAa,CAAC;CAE9C,OAAO,QAAQ,QAAQ,GAAG,GAAG;EAC3B,IAAI,aAAa,GAAG,GAClB,OAAO;EAGT,MAAM,QAAQ,GAAG;CACnB;CAEA,MAAM,IAAI,MAAM,8CAA8C;AAChE;;;ACtBA,MAAM,WAA6B;CAAC;CAAQ;CAAO;CAAQ;AAAK;AAEhE,SAAgB,uBAAuC;CACrD,MAAM,YAAY,QAAQ,IAAI,yBAAyB;CAEvD,KAAK,MAAM,WAAW,UACpB,IAAI,UAAU,WAAW,OAAO,GAC9B,OAAO;CAIX,IAAI,QAAQ,IAAI,WACd,OAAO;CAGT,OAAO;AACT;AAEA,SAAgB,sBAAsB,UAAmC;CACvE,IAAI,CAAC,UACH,OAAO,qBAAqB;CAG9B,MAAM,aAAa,SAAS,KAAK,EAAE,YAAY;CAC/C,IAAI,CAAC,SAAS,SAAS,UAA4B,GACjD,MAAM,IAAI,MAAM,gCAAgC,SAAS,gCAAgC;CAG3F,OAAO;AACT;;AAGA,SAAgB,6BAAiD;CAC/D,IAAI,QAAQ,IAAI,iBACd,OAAO,QAAQ,IAAI;CAGrB,KAAK,MAAM,OAAO,CAAC,gBAAgB,UAAU,GAAY;EACvD,MAAM,QAAQ,QAAQ,IAAI;EAC1B,IAAI,OACF,OAAO;CAEX;CAEA,MAAM,KAAK,UAAU,MAAM,CAAC,QAAQ,OAAO,GAAG,EAAE,UAAU,OAAO,CAAC;CAClE,IAAI,GAAG,WAAW,GAAG;EACnB,MAAM,QAAQ,GAAG,OAAO,KAAK;EAC7B,IAAI,OACF,OAAO;CAEX;AAGF;AAEA,SAAgB,oBAAoB,KAAa,SAA+B;CAC9E,MAAM,QAAQ,2BAA2B;CASzC,IANe,UAAU,SAAS,CAAC,SAAS,GAAG;EAC7C;EACA,OAAO;EACP,KALU,QAAQ;GAAE,GAAG,QAAQ;GAAK,iBAAiB;EAAM,IAAI,QAAQ;CAMzE,CAES,EAAE,WAAW,GACpB,MAAM,IAAI,MAAM,GAAG,QAAQ,gBAAgB;AAE/C;AAEA,SAAgB,8BAA8B,KAAmB;CAO/D,IANe,UAAU,QAAQ,CAAC,WAAW,oBAAoB,GAAG;EAClE;EACA,OAAO;EACP,KAAK,QAAQ;CACf,CAES,EAAE,WAAW,GACpB,MAAM,IAAI,MAAM,qBAAqB;AAEzC;;;;;;;AAQA,SAAgB,yBAAyB,cAA4B;CAInE,MAAM,EAAE,cAAc,OAAO,GAAG,aAAa,QAAQ;CAOrD,IANe,UAAU,QAAQ,CAAC,OAAO,OAAO,GAAG;EACjD,KAAK;EACL,OAAO;EACP,KAAK;CACP,CAES,EAAE,WAAW,GACpB,MAAM,IAAI,MAAM,yCAAyC;AAE7D;;;ACtGA,SAAgB,iBAAyB;CACvC,MAAM,kBAAkB,KAAK,eAAe,OAAO,KAAK,GAAG,GAAG,cAAc;CAE5E,OADY,KAAK,MAAM,aAAa,iBAAiB,MAAM,CAClD,EAAE,WAAW;AACxB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keystrokehq/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/keystrokehq/keystroke.git",
|
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
"conf": "^15.1.0",
|
|
30
30
|
"drizzle-orm": "^0.45.2",
|
|
31
31
|
"picomatch": "^4.0.4",
|
|
32
|
-
"tsdown": "^0.22.0"
|
|
33
|
-
"typescript": "^6.0.3"
|
|
32
|
+
"tsdown": "^0.22.0"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"@types/node": "^25.9.1",
|
|
@@ -39,11 +38,12 @@
|
|
|
39
38
|
"oxlint": "^1.66.0",
|
|
40
39
|
"tsdown": "^0.22.0",
|
|
41
40
|
"tsx": "^4.22.3",
|
|
41
|
+
"typescript": "^6.0.3",
|
|
42
42
|
"vitest": "^4.1.7",
|
|
43
43
|
"@keystrokehq/oxlint-config": "0.0.4",
|
|
44
44
|
"@keystrokehq/tsconfig": "0.0.3",
|
|
45
|
-
"@keystrokehq/tsdown-config": "0.0.
|
|
46
|
-
"@keystrokehq/vitest-config": "0.0.
|
|
45
|
+
"@keystrokehq/tsdown-config": "0.0.3",
|
|
46
|
+
"@keystrokehq/vitest-config": "0.0.6"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsx scripts/generate-catalog-versions.ts && tsdown && node scripts/copy-templates.mjs && node scripts/copy-skills-bundle.mjs",
|