@aicalas/loopy 0.2.0 → 0.2.1
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 +213 -38
- package/dist/adapters.d.ts +2 -0
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js.map +1 -1
- package/dist/authoring/effect-config.d.ts +2 -2
- package/dist/authoring/effect-config.d.ts.map +1 -1
- package/dist/authoring/effect-config.js +2 -2
- package/dist/authoring/effect-config.js.map +1 -1
- package/dist/authoring/types.d.ts +5 -0
- package/dist/authoring/types.d.ts.map +1 -1
- package/dist/cli-help.d.ts +1 -11
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +184 -134
- package/dist/cli-help.js.map +1 -1
- package/dist/cli.d.ts +2 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -46
- package/dist/cli.js.map +1 -1
- package/dist/core/calls.d.ts +3 -1
- package/dist/core/calls.d.ts.map +1 -1
- package/dist/core/calls.js +7 -2
- package/dist/core/calls.js.map +1 -1
- package/dist/core/prompts.d.ts +8 -0
- package/dist/core/prompts.d.ts.map +1 -0
- package/dist/core/prompts.js +59 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/wire.d.ts +13 -4
- package/dist/core/wire.d.ts.map +1 -1
- package/dist/core/wire.js +29 -8
- package/dist/core/wire.js.map +1 -1
- package/dist/harnesses/claude.d.ts.map +1 -1
- package/dist/harnesses/claude.js +10 -1
- package/dist/harnesses/claude.js.map +1 -1
- package/dist/harnesses/codex-app-server.d.ts +29 -0
- package/dist/harnesses/codex-app-server.d.ts.map +1 -0
- package/dist/harnesses/codex-app-server.js +166 -0
- package/dist/harnesses/codex-app-server.js.map +1 -0
- package/dist/harnesses/codex.d.ts.map +1 -1
- package/dist/harnesses/codex.js +34 -7
- package/dist/harnesses/codex.js.map +1 -1
- package/dist/harnesses/opencode.d.ts.map +1 -1
- package/dist/harnesses/opencode.js +1 -0
- package/dist/harnesses/opencode.js.map +1 -1
- package/dist/harnesses/probe.d.ts +5 -0
- package/dist/harnesses/probe.d.ts.map +1 -0
- package/dist/harnesses/probe.js +53 -0
- package/dist/harnesses/probe.js.map +1 -0
- package/dist/temporal/activities.d.ts.map +1 -1
- package/dist/temporal/activities.js +50 -10
- package/dist/temporal/activities.js.map +1 -1
- package/dist/temporal/contracts.d.ts +1 -1
- package/dist/temporal/contracts.d.ts.map +1 -1
- package/dist/temporal/contracts.js +1 -1
- package/dist/temporal/contracts.js.map +1 -1
- package/dist/temporal/handle.d.ts.map +1 -1
- package/dist/temporal/handle.js +23 -4
- package/dist/temporal/handle.js.map +1 -1
- package/dist/temporal/metadata.d.ts +7 -0
- package/dist/temporal/metadata.d.ts.map +1 -0
- package/dist/temporal/metadata.js +44 -0
- package/dist/temporal/metadata.js.map +1 -0
- package/dist/temporal/observe.d.ts +15 -2
- package/dist/temporal/observe.d.ts.map +1 -1
- package/dist/temporal/observe.js +7 -2
- package/dist/temporal/observe.js.map +1 -1
- package/dist/temporal/supervisor.d.ts +3 -1
- package/dist/temporal/supervisor.d.ts.map +1 -1
- package/dist/temporal/supervisor.js +31 -7
- package/dist/temporal/supervisor.js.map +1 -1
- package/dist/temporal/worker.d.ts.map +1 -1
- package/dist/temporal/worker.js +1 -0
- package/dist/temporal/worker.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli-help.js
CHANGED
|
@@ -1,168 +1,222 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* needs is emitted here. Two standing rules shape the copy - loopy never explains how Temporal
|
|
5
|
-
* itself works (it names the temporal-developer skill and stops), and it never teaches authoring a
|
|
6
|
-
* new adapter (there is no plugin API yet; the built-ins are configured, not defined).
|
|
7
|
-
*/
|
|
8
|
-
/** Named so an operator can find generic Temporal help; loopy-specific guidance never depends on it. */
|
|
9
|
-
export const TEMPORAL_SKILL_POINTER = "Temporal itself (server, CLI, non-determinism, Event History) is out of loopy's scope.\n" +
|
|
10
|
-
"For that, use the temporal-developer skill: https://github.com/temporalio/skill-temporal-developer";
|
|
1
|
+
/** Operator-facing guidance bundled with the CLI so it works without configuration, a Temporal connection, or network access. */
|
|
2
|
+
export const TEMPORAL_SKILL_POINTER = "Use Temporal UI or CLI for execution inspection, Event History, cancellation, termination, and Schedule administration.\n" +
|
|
3
|
+
"For Temporal operations, use the temporal-developer skill: https://github.com/temporalio/skill-temporal-developer";
|
|
11
4
|
const SETUP = {
|
|
12
5
|
name: "setup",
|
|
13
|
-
summary: "
|
|
6
|
+
summary: "install loopy, configure the local runtime, and preserve its state",
|
|
14
7
|
body: [
|
|
15
8
|
"loopy guide setup",
|
|
16
9
|
"",
|
|
17
|
-
"
|
|
18
|
-
"",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"",
|
|
23
|
-
"
|
|
24
|
-
"",
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"",
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
10
|
+
"Loopy is a local runtime for one developer. It needs Node.js 24, git, and a",
|
|
11
|
+
"reachable Temporal Service. Install the CLI globally and add the package to the",
|
|
12
|
+
"project that owns your workflows:",
|
|
13
|
+
"",
|
|
14
|
+
" npm install --global @aicalas/loopy",
|
|
15
|
+
" npm install @aicalas/loopy",
|
|
16
|
+
"",
|
|
17
|
+
"Configuration",
|
|
18
|
+
" The default file is ~/.config/loopy/config.toml",
|
|
19
|
+
" ($XDG_CONFIG_HOME/loopy/config.toml). Pass -c/--config <path> to use another file.",
|
|
20
|
+
"",
|
|
21
|
+
" log_level debug, info, warning, or error. Defaults to info.",
|
|
22
|
+
" repository Workspace repository cloned for agents. Required.",
|
|
23
|
+
" default_branch Defaults to main.",
|
|
24
|
+
" workflows Workflow-module directory. Defaults to workflows.",
|
|
25
|
+
" project_directory Base for workflows and prompt files. Defaults to .",
|
|
26
|
+
" working_directory Clone, worktree, and bundle root. Defaults to .loopy.",
|
|
27
|
+
" poll_interval_seconds Watcher and control cadence. Defaults to 300.",
|
|
28
|
+
" drain_timeout_seconds Shutdown drain deadline. Defaults to 900.",
|
|
29
|
+
" shutdown_grace_seconds Activity shutdown grace. Defaults to 30.",
|
|
30
|
+
" agent_worker.max_concurrent_activities",
|
|
31
|
+
" Local agent concurrency. Defaults to 5.",
|
|
32
|
+
" temporal.address Defaults to localhost:7233.",
|
|
33
|
+
" temporal.namespace Defaults to default.",
|
|
34
|
+
" temporal.control_task_queue Required and unique to this runtime.",
|
|
35
|
+
" temporal.agent_task_queue Required and unique to this runtime.",
|
|
36
|
+
" temporal.tls Defaults to false.",
|
|
37
|
+
"",
|
|
38
|
+
" TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, TEMPORAL_CONTROL_TASK_QUEUE,",
|
|
39
|
+
" TEMPORAL_AGENT_TASK_QUEUE, TEMPORAL_TLS, and TEMPORAL_API_KEY override their",
|
|
40
|
+
" matching Temporal fields. Loopy does not create or store secrets; provide GitHub,",
|
|
41
|
+
" Temporal, and agent-provider credentials through the environment available to loopy.",
|
|
42
|
+
"",
|
|
43
|
+
"Typical Docker Compose deployment",
|
|
44
|
+
" Run the Temporal development server and loopy as separate services. Point",
|
|
45
|
+
" TEMPORAL_ADDRESS at the Temporal service name. Mount the config, workflows, and",
|
|
46
|
+
" prompt files into loopy, and preserve three independent volumes: Temporal's database,",
|
|
47
|
+
" loopy's working_directory, and the container user's provider home. The package README",
|
|
48
|
+
" contains a complete Compose, config.toml, and Dockerfile example.",
|
|
49
|
+
"",
|
|
50
|
+
"Credentials and git authentication",
|
|
51
|
+
" GITHUB_TOKEN or GH_TOKEN authenticates GitHub watchers, effects, attention, and",
|
|
52
|
+
" questions. Git clone, fetch, and agent pushes use git's own credential mechanism;",
|
|
53
|
+
" configure an HTTPS credential helper or SSH credentials in the same environment.",
|
|
54
|
+
" Authenticate each enabled agent provider with its supported environment variables or",
|
|
55
|
+
" login flow. Never commit secret files or provider authentication state.",
|
|
56
|
+
"",
|
|
57
|
+
"Agent environment",
|
|
58
|
+
" Agents run inside the loopy process environment. A container image must therefore be a",
|
|
59
|
+
" complete development environment for the target repository: language runtimes, package",
|
|
60
|
+
" managers, build and unit-test tools, system libraries, provider CLIs, MCP server",
|
|
61
|
+
" executables, repository instructions, and network access. Initialize and authenticate",
|
|
62
|
+
" MCP servers before unattended use. Verify the repository's real test commands from an",
|
|
63
|
+
" interactive shell in that image. OpenCode additionally requires an opencode executable",
|
|
64
|
+
" at version 1.18.3 or newer on PATH.",
|
|
65
|
+
"",
|
|
66
|
+
"State that must survive restart",
|
|
67
|
+
" Temporal state The Service's persistence contains Workflow Executions, Event",
|
|
68
|
+
" Histories, Schedules, and durable call state.",
|
|
69
|
+
" Loopy config Preserve the selected config file so the same repository,",
|
|
70
|
+
" Namespace, Task Queues, and directories are used after restart.",
|
|
71
|
+
" Working directory Preserve working_directory while any execution is open. It holds",
|
|
72
|
+
" the managed clone, worktrees, bundles, and worker lock.",
|
|
73
|
+
" Provider state Preserve each provider's own session data in the same configured",
|
|
74
|
+
" home/data location. A session ID in Temporal is not the transcript.",
|
|
75
|
+
"",
|
|
76
|
+
"Run",
|
|
77
|
+
" loopy loop starts the local runtime. One process owns a working directory; worker.lock",
|
|
78
|
+
" prevents a second process from racing the same worktrees.",
|
|
51
79
|
].join("\n"),
|
|
52
80
|
};
|
|
53
81
|
const WORKFLOWS = {
|
|
54
82
|
name: "workflows",
|
|
55
|
-
summary: "
|
|
83
|
+
summary: "author workflows, inputs, agents, effects, and human questions",
|
|
56
84
|
body: [
|
|
57
85
|
"loopy guide workflows",
|
|
58
86
|
"",
|
|
59
|
-
"A workflow is a TypeScript module
|
|
60
|
-
"command
|
|
87
|
+
"A workflow is a TypeScript module. There is no specification language or registration",
|
|
88
|
+
"command: loopy loop discovers the modules in the configured workflows directory.",
|
|
61
89
|
"",
|
|
62
|
-
"
|
|
63
|
-
" One
|
|
90
|
+
"Built-in input",
|
|
91
|
+
" One module default-exports one workflow definition:",
|
|
64
92
|
"",
|
|
65
93
|
" import { claude, defineWorkflow, githubAttention, githubPullRequests }",
|
|
66
94
|
' from "@aicalas/loopy/workflow";',
|
|
67
95
|
"",
|
|
96
|
+
" const outcome = {",
|
|
97
|
+
' type: "object",',
|
|
98
|
+
' properties: { summary: { type: "string" } },',
|
|
99
|
+
' required: ["summary"],',
|
|
100
|
+
" additionalProperties: false,",
|
|
101
|
+
" } as const;",
|
|
102
|
+
"",
|
|
68
103
|
" export default defineWorkflow({",
|
|
69
104
|
' name: "review",',
|
|
70
105
|
' watcher: githubPullRequests({ repository: "owner/repo", label: "agent:review" }),',
|
|
71
106
|
' attention: githubAttention({ target: "trigger" }),',
|
|
72
107
|
" async run(ctx) {",
|
|
73
|
-
' const agent = ctx.agent("reviewer",
|
|
74
|
-
'
|
|
108
|
+
' const agent = ctx.agent("reviewer",',
|
|
109
|
+
' claude({ model: "claude-opus-4-8", effort: "high" }));',
|
|
110
|
+
' ctx.progress("Reviewing the pull request");',
|
|
111
|
+
' return await agent.run({ output: outcome, prompt: ["Review this pull request."] });',
|
|
75
112
|
" },",
|
|
76
113
|
" });",
|
|
77
114
|
"",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"",
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"",
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"",
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"",
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"",
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
115
|
+
" A GitHub watcher claims each labelled object before run(ctx) starts. A workflow may",
|
|
116
|
+
" instead declare a Temporal schedule with cron or intervalSeconds. A workflow declares",
|
|
117
|
+
" exactly one intake; watcher and schedule are the built-in choices. Attention is optional",
|
|
118
|
+
" for scheduled workflows.",
|
|
119
|
+
"",
|
|
120
|
+
"Authoring boundaries",
|
|
121
|
+
" Workflow code runs in Temporal's deterministic isolate. Keep top-level module evaluation",
|
|
122
|
+
" free of side effects; loopy imports modules under Node for discovery and again when it",
|
|
123
|
+
" builds the isolate bundle. Filesystem, network, provider, and git work belong behind",
|
|
124
|
+
" Activities such as inputs, agent harnesses, effects, attention, and channels.",
|
|
125
|
+
"",
|
|
126
|
+
" ctx.agent(id, harness) keeps one worktree and provider session across calls.",
|
|
127
|
+
" ctx.effect(id, effect) performs a declared idempotent external mutation once per id.",
|
|
128
|
+
" ctx.ask(id, channel, request) parks without occupying an agent slot until answered.",
|
|
129
|
+
" ctx.progress(details) replaces Temporal Current Details with the latest message.",
|
|
130
|
+
" Agent output accepts a Standard JSON Schema validator or literal JSON Schema.",
|
|
131
|
+
"",
|
|
132
|
+
"Built-in adapters",
|
|
133
|
+
" githubIssues / githubPullRequests Poll labelled GitHub objects. Edge mode consumes the",
|
|
134
|
+
" intake label; level mode repeats while it remains.",
|
|
135
|
+
" claude / codex / opencode Run a named resumable provider session in a worktree.",
|
|
136
|
+
" githubAttention Report halted, parked, stuck, and awaiting-answer runs.",
|
|
137
|
+
" githubLabels Apply an idempotent label change through ctx.effect.",
|
|
138
|
+
" githubComments Ask through ctx.ask and resume from a matching reply.",
|
|
139
|
+
" promptFile Read and interpolate a prompt outside the isolate.",
|
|
140
|
+
"",
|
|
141
|
+
" Harness configuration controls permissions and sandboxing. By default provider settings",
|
|
142
|
+
" and repository instructions remain available, including their tools and MCP servers.",
|
|
143
|
+
" Treat those settings as part of the workflow's effective capability and credential scope.",
|
|
144
|
+
"",
|
|
145
|
+
"Prompt files",
|
|
146
|
+
" promptFile(path, values?) works in agent prompts and ctx.ask human questions. Paths",
|
|
147
|
+
" resolve against project_directory outside the isolate. Files use named double-brace",
|
|
148
|
+
" placeholders such as {{ repository }}; names begin with a letter or underscore and may",
|
|
149
|
+
" continue with letters, digits, or underscores. Whitespace inside the braces is ignored.",
|
|
150
|
+
"",
|
|
151
|
+
' promptFile("prompts/review.md", { repository: ctx.trigger.repository, issue: 42 })',
|
|
152
|
+
"",
|
|
153
|
+
" The values map is the template's only input. Strings are inserted verbatim; finite",
|
|
154
|
+
" numbers, booleans, and null use JSON scalar text. Unused values are allowed, but every",
|
|
155
|
+
" placeholder needs a value. Missing values, malformed placeholders, non-finite numbers,",
|
|
156
|
+
" and objects or arrays halt without retry before an agent or question channel is called.",
|
|
157
|
+
"",
|
|
158
|
+
"Deploy an edit",
|
|
159
|
+
" Workflow modules are bundled once when loopy loop starts. Send SIGINT or SIGTERM to pause",
|
|
160
|
+
" intake and drain open executions, then restart. A second signal skips the drain. Loopy has",
|
|
161
|
+
" no workflow versioning, so do not put an open execution under incompatible workflow code.",
|
|
113
162
|
].join("\n"),
|
|
114
163
|
};
|
|
115
|
-
const
|
|
116
|
-
name: "
|
|
117
|
-
summary: "
|
|
164
|
+
const RECOVERY = {
|
|
165
|
+
name: "recovery",
|
|
166
|
+
summary: "resume the local runtime and recover parked or interrupted work",
|
|
118
167
|
body: [
|
|
119
|
-
"loopy guide
|
|
120
|
-
"",
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"",
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"",
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"",
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
" the
|
|
168
|
+
"loopy guide recovery",
|
|
169
|
+
"",
|
|
170
|
+
"Temporal preserves orchestration state; the local filesystem preserves the material an",
|
|
171
|
+
"agent needs to continue. Recovery requires the same Temporal Namespace and Task Queues,",
|
|
172
|
+
"the same loopy config, the same working_directory, and the provider's session data.",
|
|
173
|
+
"",
|
|
174
|
+
"Runtime restart",
|
|
175
|
+
" A graceful signal drains before exit. After an ordinary process or machine restart, run",
|
|
176
|
+
" loopy loop with the same config and storage. Temporal schedules new tasks for the restored",
|
|
177
|
+
" worker, and open executions continue. Closed executions need no local workspace.",
|
|
178
|
+
"",
|
|
179
|
+
"Agent-call outcomes",
|
|
180
|
+
" Explicit provider, validation, deadline, or session-resume failure parks the call for",
|
|
181
|
+
" recovery and retains its known session. Fix the cause, then run:",
|
|
182
|
+
"",
|
|
183
|
+
" loopy resolve <workflow-id> --retry",
|
|
184
|
+
"",
|
|
185
|
+
" Worker loss, heartbeat timeout, or a lost result is ambiguous because the old provider",
|
|
186
|
+
" invocation may have changed the worktree or remote repository. Confirm that it can no",
|
|
187
|
+
" longer mutate state before retrying. Loopy reuses the last session ID checkpointed in",
|
|
188
|
+
" Temporal when available; otherwise it retains an earlier known session or starts fresh.",
|
|
189
|
+
" The resolve result shows sessionId on the parked call. Use --halt when retry is unsafe.",
|
|
190
|
+
"",
|
|
191
|
+
" If several calls are parked, add --call <handle>/call/<n>. If ctx.ask is waiting and its",
|
|
192
|
+
" configured channel is unavailable, use loopy answer <workflow-id> <text>, with --call when",
|
|
193
|
+
" needed.",
|
|
194
|
+
"",
|
|
195
|
+
"Lost storage",
|
|
196
|
+
" A retained Temporal history cannot reconstruct an absent worktree or provider transcript.",
|
|
197
|
+
" Do not retry a session against a different path or empty provider state and assume it is",
|
|
198
|
+
" continuous. Inspect the execution and workspace, preserve any recoverable git work, then",
|
|
199
|
+
" terminate or reset through Temporal and start fresh. Remove orphaned worktrees manually",
|
|
200
|
+
" only after no open execution owns them.",
|
|
201
|
+
"",
|
|
202
|
+
"Temporal operations",
|
|
203
|
+
" Use Temporal UI or CLI to inspect executions and Event Histories, diagnose Workflow Task",
|
|
204
|
+
" failures, administer Schedules, and cancel, reset, or terminate executions. Loopy's",
|
|
205
|
+
" resolve and answer commands exist only for Loopy's parked-call and human-input contracts.",
|
|
150
206
|
].join("\n"),
|
|
151
207
|
};
|
|
152
|
-
|
|
153
|
-
export const GUIDE_TOPICS = [SETUP, WORKFLOWS, MONITOR];
|
|
208
|
+
export const GUIDE_TOPICS = [SETUP, WORKFLOWS, RECOVERY];
|
|
154
209
|
export function guideTopicNames() {
|
|
155
210
|
return GUIDE_TOPICS.map((topic) => topic.name);
|
|
156
211
|
}
|
|
157
212
|
export function findGuideTopic(name) {
|
|
158
213
|
return GUIDE_TOPICS.find((topic) => topic.name === name);
|
|
159
214
|
}
|
|
160
|
-
/** `loopy guide` with no topic: the topic list plus the one external pointer loopy makes. */
|
|
161
215
|
export function renderGuideIndex() {
|
|
162
216
|
const width = Math.max(...GUIDE_TOPICS.map((topic) => topic.name.length));
|
|
163
217
|
const rows = GUIDE_TOPICS.map((topic) => ` ${topic.name.padEnd(width)} ${topic.summary}`).join("\n");
|
|
164
218
|
return [
|
|
165
|
-
"loopy guide <topic> -
|
|
219
|
+
"loopy guide <topic> - offline guidance with no config or Temporal connection required.",
|
|
166
220
|
"",
|
|
167
221
|
"Topics:",
|
|
168
222
|
rows,
|
|
@@ -170,19 +224,15 @@ export function renderGuideIndex() {
|
|
|
170
224
|
TEMPORAL_SKILL_POINTER,
|
|
171
225
|
].join("\n");
|
|
172
226
|
}
|
|
173
|
-
/** Appended after `loopy --help`: the playbook that points at the guide topics. */
|
|
174
227
|
export function helpPlaybook() {
|
|
175
228
|
return [
|
|
176
229
|
"Playbook:",
|
|
177
|
-
" loopy loop Run
|
|
178
|
-
" loopy
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"",
|
|
182
|
-
" To deploy a workflow change: edit files, then restart loopy loop.",
|
|
230
|
+
" loopy loop Run workflows and drain safely when interrupted.",
|
|
231
|
+
" loopy resolve <id> --retry Resume or reconcile a parked agent call.",
|
|
232
|
+
' loopy answer <id> "<text>" Answer a workflow waiting for human input.',
|
|
233
|
+
" loopy guide Read setup, authoring, and recovery guidance.",
|
|
183
234
|
"",
|
|
184
|
-
"
|
|
185
|
-
" or run loopy guide for the topic list.",
|
|
235
|
+
"For inspection, histories, cancellation, and termination, use Temporal UI or CLI.",
|
|
186
236
|
].join("\n");
|
|
187
237
|
}
|
|
188
238
|
//# sourceMappingURL=cli-help.js.map
|
package/dist/cli-help.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-help.js","sourceRoot":"","sources":["../src/cli-help.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cli-help.js","sourceRoot":"","sources":["../src/cli-help.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
2
3
|
import type { AdapterRegistries } from "./adapters.js";
|
|
3
4
|
import { buildWorkflowBundle, discoverWorkflows } from "./build/discover.js";
|
|
4
5
|
import { type InstanceState, type ResolveRequest } from "./core/wire.js";
|
|
5
6
|
import { ensureRepo } from "./git.js";
|
|
6
7
|
import { type Settings } from "./settings.js";
|
|
7
8
|
import { connectClient } from "./temporal/client.js";
|
|
8
|
-
import { type InstanceView } from "./temporal/status.js";
|
|
9
9
|
import { createWorkerRuntime } from "./temporal/worker.js";
|
|
10
10
|
export interface CliServices {
|
|
11
11
|
serve(settings: Settings, registries: AdapterRegistries): Promise<void>;
|
|
12
12
|
resolve(workflowId: string, request: ResolveOptions, settings: Settings): Promise<InstanceState>;
|
|
13
13
|
answer(workflowId: string, request: AnswerOptions, settings: Settings): Promise<InstanceState>;
|
|
14
|
-
status(settings: Settings, workflowId: string | undefined): Promise<InstanceView[]>;
|
|
15
14
|
}
|
|
16
15
|
export interface ServeServices {
|
|
17
16
|
ensureRepo: typeof ensureRepo;
|
|
@@ -28,10 +27,10 @@ export interface AnswerOptions {
|
|
|
28
27
|
answer: string;
|
|
29
28
|
callId?: string;
|
|
30
29
|
}
|
|
30
|
+
export declare function createCli(services: CliServices): Command;
|
|
31
31
|
export declare function main(argv: readonly string[], services: CliServices): Promise<void>;
|
|
32
32
|
export declare function serve(settings: Settings, registries: AdapterRegistries, services: ServeServices): Promise<void>;
|
|
33
33
|
export declare function resolveInstance(workflowId: string, request: ResolveOptions, settings: Settings, connect?: typeof connectClient): Promise<InstanceState>;
|
|
34
34
|
export declare function answerInstance(workflowId: string, request: AnswerOptions, settings: Settings, connect?: typeof connectClient): Promise<InstanceState>;
|
|
35
|
-
export declare function statusInstances(settings: Settings, workflowId: string | undefined, connect?: typeof connectClient): Promise<InstanceView[]>;
|
|
36
35
|
export declare function runExecutable(argv: readonly string[], services: CliServices): Promise<void>;
|
|
37
36
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,OAAO,EACL,mBAAmB,EAEnB,iBAAiB,EAClB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAEL,KAAK,aAAa,EAElB,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAY,UAAU,EAAE,MAAM,UAAU,CAAC;AAEhD,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE5D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AASrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAM3D,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACjG,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAChG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;CACjD;AAoBD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAwFxD;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAExF;AAED,wBAAsB,KAAK,CACzB,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,iBAAiB,EAC7B,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC,CAkEf;AAED,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,OAAO,aAA6B,GAC5C,OAAO,CAAC,aAAa,CAAC,CAcxB;AAED,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,OAAO,aAA6B,GAC5C,OAAO,CAAC,aAAa,CAAC,CAcxB;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAWjG"}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { WorkflowNotFoundError } from "@temporalio/client";
|
|
5
4
|
import { Runtime } from "@temporalio/worker";
|
|
6
5
|
import { Command, Option } from "commander";
|
|
7
6
|
import { createAdapterRegistries } from "./adapters.js";
|
|
@@ -17,7 +16,6 @@ import { ANSWER_INSTANCE_UPDATE, INSTANCE_STATE_QUERY, RESOLVE_INSTANCE_UPDATE,
|
|
|
17
16
|
import { drainOnSignal } from "./temporal/drain.js";
|
|
18
17
|
import { countOpenExecutions } from "./temporal/executions.js";
|
|
19
18
|
import { resumeDrainedSchedules, syncSchedules } from "./temporal/schedules.js";
|
|
20
|
-
import { describeInstance, listInstances, } from "./temporal/status.js";
|
|
21
19
|
import { createWorkerRuntime } from "./temporal/worker.js";
|
|
22
20
|
import { LOOPY_VERSION } from "./version.js";
|
|
23
21
|
import { WorkerLockError, WorkerProcessLock } from "./worker-lock.js";
|
|
@@ -34,12 +32,11 @@ const DEFAULT_CLI_SERVICES = {
|
|
|
34
32
|
serve: (settings, registries) => serve(settings, registries, DEFAULT_SERVE_SERVICES),
|
|
35
33
|
resolve: resolveInstance,
|
|
36
34
|
answer: answerInstance,
|
|
37
|
-
status: statusInstances,
|
|
38
35
|
};
|
|
39
|
-
export
|
|
36
|
+
export function createCli(services) {
|
|
40
37
|
const program = new Command()
|
|
41
38
|
.name("loopy")
|
|
42
|
-
.description("
|
|
39
|
+
.description("Run durable local agent workflows on Temporal.")
|
|
43
40
|
.version(LOOPY_VERSION)
|
|
44
41
|
.option("-c, --config <path>", "loopy config TOML (default: ~/.config/loopy/config.toml)")
|
|
45
42
|
.addOption(new Option("--log-level <level>", "override the configured log level").choices([
|
|
@@ -48,7 +45,7 @@ export async function main(argv, services) {
|
|
|
48
45
|
program.addHelpText("after", `\n${helpPlaybook()}`);
|
|
49
46
|
program
|
|
50
47
|
.command("guide")
|
|
51
|
-
.description("
|
|
48
|
+
.description("Learn how to configure, author, run, and recover workflows.")
|
|
52
49
|
.argument("[topic]", `one of: ${guideTopicNames().join(", ")}`)
|
|
53
50
|
.action((topic) => {
|
|
54
51
|
if (topic === undefined) {
|
|
@@ -63,7 +60,7 @@ export async function main(argv, services) {
|
|
|
63
60
|
});
|
|
64
61
|
program
|
|
65
62
|
.command("loop")
|
|
66
|
-
.description("Run
|
|
63
|
+
.description("Run workflows until interrupted.")
|
|
67
64
|
.option("-w, --workflows <path>", "directory of workflow modules (overrides the config)")
|
|
68
65
|
.action(async (loopOptions) => {
|
|
69
66
|
const options = program.opts();
|
|
@@ -73,7 +70,7 @@ export async function main(argv, services) {
|
|
|
73
70
|
});
|
|
74
71
|
program
|
|
75
72
|
.command("resolve")
|
|
76
|
-
.description("Resume
|
|
73
|
+
.description("Resume or reconcile a parked agent call.")
|
|
77
74
|
.argument("<workflow-id>", "instance Workflow ID")
|
|
78
75
|
.option("--call <address>", "call address, for example reviewer/call/2")
|
|
79
76
|
.option("--retry", "retry the call (an ambiguous prior call must no longer mutate state)")
|
|
@@ -94,7 +91,7 @@ export async function main(argv, services) {
|
|
|
94
91
|
});
|
|
95
92
|
program
|
|
96
93
|
.command("answer")
|
|
97
|
-
.description("Answer
|
|
94
|
+
.description("Answer a workflow waiting for human input.")
|
|
98
95
|
.argument("<workflow-id>", "instance Workflow ID")
|
|
99
96
|
.argument("<text>", "the answer text delivered to the waiting ask")
|
|
100
97
|
.option("--call <address>", "call address, for example confirm-drop/call/1")
|
|
@@ -109,19 +106,10 @@ export async function main(argv, services) {
|
|
|
109
106
|
const state = await services.answer(workflowId, request, settings);
|
|
110
107
|
process.stdout.write(`${JSON.stringify(state)}\n`);
|
|
111
108
|
});
|
|
112
|
-
program
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
.action(async (workflowId) => {
|
|
117
|
-
const options = program.opts();
|
|
118
|
-
const settings = loadSettings(options.config);
|
|
119
|
-
startLogging(settings, options.logLevel);
|
|
120
|
-
const views = await services.status(settings, workflowId);
|
|
121
|
-
for (const view of views)
|
|
122
|
-
process.stdout.write(`${JSON.stringify(view)}\n`);
|
|
123
|
-
});
|
|
124
|
-
await program.parseAsync([...argv]);
|
|
109
|
+
return program;
|
|
110
|
+
}
|
|
111
|
+
export async function main(argv, services) {
|
|
112
|
+
await createCli(services).parseAsync([...argv]);
|
|
125
113
|
}
|
|
126
114
|
export async function serve(settings, registries, services) {
|
|
127
115
|
const processLock = new WorkerProcessLock(join(settings.workingDirectory, "worker.lock"));
|
|
@@ -218,30 +206,6 @@ export async function answerInstance(workflowId, request, settings, connect = co
|
|
|
218
206
|
await connection.close();
|
|
219
207
|
}
|
|
220
208
|
}
|
|
221
|
-
export async function statusInstances(settings, workflowId, connect = connectClient) {
|
|
222
|
-
const connection = await connect(settings.temporal);
|
|
223
|
-
try {
|
|
224
|
-
const client = connection.client.workflow;
|
|
225
|
-
if (workflowId !== undefined) {
|
|
226
|
-
try {
|
|
227
|
-
return [await describeInstance(client, workflowId)];
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
// A listing only describes IDs Visibility just returned, but an explicit ID comes from an
|
|
231
|
-
// operator and may be stale or mistyped. Report that as a clean CLI error rather than
|
|
232
|
-
// letting the raw WorkflowNotFoundError escape as an unhandled stack trace.
|
|
233
|
-
if (error instanceof WorkflowNotFoundError) {
|
|
234
|
-
throw new CliError(`no loopy instance found with ID ${JSON.stringify(workflowId)}`);
|
|
235
|
-
}
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return await listInstances(client);
|
|
240
|
-
}
|
|
241
|
-
finally {
|
|
242
|
-
await connection.close();
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
209
|
export async function runExecutable(argv, services) {
|
|
246
210
|
try {
|
|
247
211
|
await main(argv, services);
|