@intentius/chant 0.1.6 → 0.1.7
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/package.json +1 -1
- package/src/cli/commands/build.ts +17 -0
- package/src/cli/handlers/graph.ts +23 -0
- package/src/cli/handlers/run-client.ts +134 -0
- package/src/cli/handlers/run-report.ts +160 -0
- package/src/cli/handlers/run.ts +453 -0
- package/src/cli/main.test.ts +64 -0
- package/src/cli/main.ts +28 -18
- package/src/cli/mcp/op-tools.ts +204 -0
- package/src/cli/mcp/resource-handlers.ts +69 -50
- package/src/cli/mcp/resources/context.ts +27 -0
- package/src/cli/mcp/server.test.ts +176 -3
- package/src/cli/mcp/server.ts +7 -3
- package/src/cli/mcp/state-tools.ts +0 -51
- package/src/cli/registry.ts +2 -0
- package/src/composite.ts +10 -5
- package/src/index.ts +1 -2
- package/src/op/discover.test.ts +43 -0
- package/src/op/discover.ts +89 -0
- package/src/op/index.ts +3 -1
- package/src/op/types.ts +0 -6
- package/src/cli/handlers/spell.ts +0 -396
- package/src/spell/discovery.ts +0 -183
- package/src/spell/index.ts +0 -3
- package/src/spell/prompt.ts +0 -133
- package/src/spell/types.ts +0 -89
package/package.json
CHANGED
|
@@ -101,6 +101,23 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// Empty-output guard: source files were discovered but no lexicon produced
|
|
105
|
+
// any output. Almost always indicates broken imports resolving to undefined
|
|
106
|
+
// (e.g. missing root re-exports from a lexicon) or modules that exported no
|
|
107
|
+
// Declarables. Without this guard, chant writes "{}" and exits 0.
|
|
108
|
+
if (
|
|
109
|
+
result.sourceFileCount > 0 &&
|
|
110
|
+
result.outputs.size === 0 &&
|
|
111
|
+
result.errors.length === 0 &&
|
|
112
|
+
errors.length === 0
|
|
113
|
+
) {
|
|
114
|
+
errors.push(
|
|
115
|
+
formatError({
|
|
116
|
+
message: `Discovered ${result.sourceFileCount} source file(s) but produced no output. Likely causes: imports resolving to undefined (missing exports from a lexicon root), or no Declarables/Composites exported. Check that imported names exist in the target package.`,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
// Handle output
|
|
105
122
|
if (result.errors.length === 0 && errors.length === 0) {
|
|
106
123
|
// Extract primary content and collect additional files from SerializerResult
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { discoverOps } from "../../op/discover";
|
|
2
|
+
import { formatError } from "../format";
|
|
3
|
+
import type { CommandContext } from "../registry";
|
|
4
|
+
|
|
5
|
+
export async function runGraph(_ctx: CommandContext): Promise<number> {
|
|
6
|
+
const { ops, errors } = await discoverOps();
|
|
7
|
+
for (const err of errors) console.error(formatError({ message: err }));
|
|
8
|
+
|
|
9
|
+
if (ops.size === 0) {
|
|
10
|
+
console.log("No Ops found");
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let hasEdges = false;
|
|
15
|
+
for (const [name, { config }] of ops) {
|
|
16
|
+
for (const dep of config.depends ?? []) {
|
|
17
|
+
console.log(`${dep} → ${name}`);
|
|
18
|
+
hasEdges = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!hasEdges) console.log("No Op dependencies");
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** Subset of WorkerProfile fields used by `chant run`. */
|
|
2
|
+
export interface WorkerProfile {
|
|
3
|
+
address: string;
|
|
4
|
+
namespace: string;
|
|
5
|
+
taskQueue: string;
|
|
6
|
+
tls?: boolean | { serverNameOverride?: string };
|
|
7
|
+
apiKey?: string | { env: string };
|
|
8
|
+
autoStart?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TemporalClientModule {
|
|
12
|
+
Connection: {
|
|
13
|
+
connect(opts: Record<string, unknown>): Promise<unknown>;
|
|
14
|
+
};
|
|
15
|
+
Client: new (opts: Record<string, unknown>) => TemporalClientHandle;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TemporalClientHandle {
|
|
19
|
+
workflow: {
|
|
20
|
+
start(workflowFn: unknown, opts: Record<string, unknown>): Promise<WorkflowHandleRaw>;
|
|
21
|
+
getHandle(workflowId: string): WorkflowHandleRaw;
|
|
22
|
+
list(opts?: Record<string, unknown>): AsyncIterable<WorkflowExecutionInfo>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WorkflowHandleRaw {
|
|
27
|
+
workflowId: string;
|
|
28
|
+
firstExecutionRunId?: string;
|
|
29
|
+
result(): Promise<unknown>;
|
|
30
|
+
describe(): Promise<WorkflowExecutionDescription>;
|
|
31
|
+
fetchHistory(): Promise<WorkflowHistoryRaw>;
|
|
32
|
+
signal(signalName: string): Promise<void>;
|
|
33
|
+
cancel(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WorkflowExecutionDescription {
|
|
37
|
+
workflowId: string;
|
|
38
|
+
runId: string;
|
|
39
|
+
status: { name: string };
|
|
40
|
+
startTime: Date;
|
|
41
|
+
closeTime?: Date;
|
|
42
|
+
taskQueue: string;
|
|
43
|
+
type: { name: string };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WorkflowExecutionInfo {
|
|
47
|
+
workflowId: string;
|
|
48
|
+
runId: string;
|
|
49
|
+
type: { name: string };
|
|
50
|
+
status: { name: string };
|
|
51
|
+
startTime: Date;
|
|
52
|
+
closeTime?: Date;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorkflowHistoryRaw {
|
|
56
|
+
events?: HistoryEvent[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface HistoryEvent {
|
|
60
|
+
eventType?: string;
|
|
61
|
+
eventTime?: Date;
|
|
62
|
+
activityTaskCompletedEventAttributes?: { scheduledEventId?: string | number };
|
|
63
|
+
activityTaskScheduledEventAttributes?: { activityId?: string; activityType?: { name?: string } };
|
|
64
|
+
activityTaskFailedEventAttributes?: { failure?: { message?: string } };
|
|
65
|
+
workflowExecutionCompletedEventAttributes?: unknown;
|
|
66
|
+
workflowExecutionFailedEventAttributes?: { failure?: { message?: string } };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Dynamically import @temporalio/client from the user's project node_modules.
|
|
71
|
+
* Fails with a helpful message if not installed.
|
|
72
|
+
*/
|
|
73
|
+
export async function loadTemporalClient(): Promise<TemporalClientModule> {
|
|
74
|
+
try {
|
|
75
|
+
// Use variable to prevent tsc from statically resolving the optional dep
|
|
76
|
+
const mod = "@temporalio/client";
|
|
77
|
+
return await import(mod) as unknown as TemporalClientModule;
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'@temporalio/client is not installed. Run: npm install @temporalio/client',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build a Temporal Connection.connect() options object from a worker profile.
|
|
87
|
+
*/
|
|
88
|
+
export function connectionOptions(profile: WorkerProfile): Record<string, unknown> {
|
|
89
|
+
const apiKey =
|
|
90
|
+
typeof profile.apiKey === "object" && profile.apiKey !== null
|
|
91
|
+
? process.env[(profile.apiKey as { env: string }).env]
|
|
92
|
+
: (profile.apiKey as string | undefined);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
address: profile.address,
|
|
96
|
+
...(profile.tls && {
|
|
97
|
+
tls: typeof profile.tls === "object" ? profile.tls : {},
|
|
98
|
+
metadata: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Deterministic workflow ID for an Op — allows status/signal/cancel/log
|
|
105
|
+
* without storing run IDs locally.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveWorkflowId(opName: string): string {
|
|
108
|
+
return `chant-op-${opName}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve a named profile from the chant config.
|
|
113
|
+
* Falls back to defaultProfile then "local".
|
|
114
|
+
*/
|
|
115
|
+
export function resolveProfile(
|
|
116
|
+
config: Record<string, unknown>,
|
|
117
|
+
profileName?: string,
|
|
118
|
+
): WorkerProfile {
|
|
119
|
+
const temporal = config.temporal as Record<string, unknown> | undefined;
|
|
120
|
+
if (!temporal?.profiles) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'No temporal.profiles found in chant.config.ts. Add a profile to use `chant run`.',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const profiles = temporal.profiles as Record<string, WorkerProfile>;
|
|
126
|
+
const name = profileName ?? (temporal.defaultProfile as string | undefined) ?? "local";
|
|
127
|
+
const profile = profiles[name];
|
|
128
|
+
if (!profile) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Temporal profile "${name}" not found. Available: ${Object.keys(profiles).join(", ")}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return profile;
|
|
134
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import type { OpConfig } from "../../op/types";
|
|
4
|
+
import type { WorkflowExecutionDescription, WorkflowHistoryRaw, HistoryEvent } from "./run-client";
|
|
5
|
+
|
|
6
|
+
interface ActivityRecord {
|
|
7
|
+
name: string;
|
|
8
|
+
startTime?: Date;
|
|
9
|
+
endTime?: Date;
|
|
10
|
+
durationMs?: number;
|
|
11
|
+
status: "completed" | "failed" | "running";
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PhaseRecord {
|
|
16
|
+
name: string;
|
|
17
|
+
activities: ActivityRecord[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatDuration(ms: number): string {
|
|
21
|
+
if (ms < 1000) return `${ms}ms`;
|
|
22
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
23
|
+
const mins = Math.floor(ms / 60_000);
|
|
24
|
+
const secs = Math.round((ms % 60_000) / 1000);
|
|
25
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractPhaseRecords(config: OpConfig, history: WorkflowHistoryRaw): PhaseRecord[] {
|
|
29
|
+
const events = history.events ?? [];
|
|
30
|
+
|
|
31
|
+
// Build map: scheduled event ID → activity name
|
|
32
|
+
const scheduledActivities = new Map<string, string>();
|
|
33
|
+
const scheduledTimes = new Map<string, Date>();
|
|
34
|
+
const completedTimes = new Map<string, Date>();
|
|
35
|
+
const failedActivities = new Map<string, string>();
|
|
36
|
+
|
|
37
|
+
for (const event of events) {
|
|
38
|
+
if (event.eventType === "ActivityTaskScheduled" && event.activityTaskScheduledEventAttributes) {
|
|
39
|
+
const attrs = event.activityTaskScheduledEventAttributes;
|
|
40
|
+
const id = String(attrs.activityId ?? "");
|
|
41
|
+
const name = attrs.activityType?.name ?? "unknown";
|
|
42
|
+
scheduledActivities.set(id, name);
|
|
43
|
+
if (event.eventTime) scheduledTimes.set(id, new Date(event.eventTime));
|
|
44
|
+
}
|
|
45
|
+
if (event.eventType === "ActivityTaskCompleted" && event.activityTaskCompletedEventAttributes) {
|
|
46
|
+
const scheduledId = String(event.activityTaskCompletedEventAttributes.scheduledEventId ?? "");
|
|
47
|
+
if (event.eventTime) completedTimes.set(scheduledId, new Date(event.eventTime));
|
|
48
|
+
}
|
|
49
|
+
if (event.eventType === "ActivityTaskFailed" && event.activityTaskFailedEventAttributes) {
|
|
50
|
+
const msg = event.activityTaskFailedEventAttributes.failure?.message ?? "unknown error";
|
|
51
|
+
failedActivities.set(String(events.indexOf(event)), msg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return config.phases.map((phase) => {
|
|
56
|
+
const activities: ActivityRecord[] = phase.steps
|
|
57
|
+
.filter((s) => s.kind === "activity")
|
|
58
|
+
.map((step) => {
|
|
59
|
+
if (step.kind !== "activity") return null;
|
|
60
|
+
const fn = step.fn;
|
|
61
|
+
// Find this activity by name in the history
|
|
62
|
+
let record: ActivityRecord | null = null;
|
|
63
|
+
for (const [id, name] of scheduledActivities) {
|
|
64
|
+
if (name === fn) {
|
|
65
|
+
const start = scheduledTimes.get(id);
|
|
66
|
+
const end = completedTimes.get(id);
|
|
67
|
+
const durationMs = start && end ? end.getTime() - start.getTime() : undefined;
|
|
68
|
+
record = {
|
|
69
|
+
name: fn,
|
|
70
|
+
startTime: start,
|
|
71
|
+
endTime: end,
|
|
72
|
+
durationMs,
|
|
73
|
+
status: end ? "completed" : "running",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return record ?? { name: fn, status: "running" };
|
|
78
|
+
})
|
|
79
|
+
.filter((r): r is ActivityRecord => r !== null);
|
|
80
|
+
|
|
81
|
+
return { name: phase.name, activities };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function generateReport(
|
|
86
|
+
opName: string,
|
|
87
|
+
config: OpConfig,
|
|
88
|
+
description: WorkflowExecutionDescription,
|
|
89
|
+
history: WorkflowHistoryRaw,
|
|
90
|
+
): string {
|
|
91
|
+
const lines: string[] = [];
|
|
92
|
+
|
|
93
|
+
const status = description.status.name;
|
|
94
|
+
const startTime = description.startTime;
|
|
95
|
+
const closeTime = description.closeTime;
|
|
96
|
+
const durationMs = closeTime ? closeTime.getTime() - startTime.getTime() : undefined;
|
|
97
|
+
|
|
98
|
+
lines.push(`# Deployment Report: ${opName}`);
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push("## Overview");
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push(`| Field | Value |`);
|
|
103
|
+
lines.push(`|---|---|`);
|
|
104
|
+
lines.push(`| Op | ${opName} |`);
|
|
105
|
+
lines.push(`| Overview | ${config.overview} |`);
|
|
106
|
+
lines.push(`| Status | **${status}** |`);
|
|
107
|
+
lines.push(`| Workflow ID | ${description.workflowId} |`);
|
|
108
|
+
lines.push(`| Run ID | ${description.runId} |`);
|
|
109
|
+
lines.push(`| Start | ${startTime.toISOString()} |`);
|
|
110
|
+
if (closeTime) lines.push(`| End | ${closeTime.toISOString()} |`);
|
|
111
|
+
if (durationMs !== undefined) lines.push(`| Duration | ${formatDuration(durationMs)} |`);
|
|
112
|
+
lines.push("");
|
|
113
|
+
|
|
114
|
+
lines.push("## Timeline");
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push("| Phase | Activity | Duration | Status |");
|
|
117
|
+
lines.push("|---|---|---|---|");
|
|
118
|
+
|
|
119
|
+
const phases = extractPhaseRecords(config, history);
|
|
120
|
+
for (const phase of phases) {
|
|
121
|
+
if (phase.activities.length === 0) {
|
|
122
|
+
lines.push(`| ${phase.name} | — | — | — |`);
|
|
123
|
+
}
|
|
124
|
+
for (const act of phase.activities) {
|
|
125
|
+
const dur = act.durationMs !== undefined ? formatDuration(act.durationMs) : "—";
|
|
126
|
+
const statusEmoji = act.status === "completed" ? "✓" : act.status === "failed" ? "✗" : "…";
|
|
127
|
+
lines.push(`| ${phase.name} | ${act.name} | ${dur} | ${statusEmoji} ${act.status} |`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
|
|
132
|
+
// Errors section
|
|
133
|
+
const failedEvents = (history.events ?? []).filter(
|
|
134
|
+
(e): e is HistoryEvent & Required<Pick<HistoryEvent, "activityTaskFailedEventAttributes">> =>
|
|
135
|
+
e.eventType === "ActivityTaskFailed" && !!e.activityTaskFailedEventAttributes,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (failedEvents.length > 0) {
|
|
139
|
+
lines.push("## Errors");
|
|
140
|
+
lines.push("");
|
|
141
|
+
for (const event of failedEvents) {
|
|
142
|
+
const msg = event.activityTaskFailedEventAttributes?.failure?.message ?? "unknown";
|
|
143
|
+
lines.push(`- ${msg}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push("");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
lines.push(`---`);
|
|
149
|
+
lines.push(`*Generated by chant at ${new Date().toISOString()}*`);
|
|
150
|
+
lines.push("");
|
|
151
|
+
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function writeReport(opName: string, markdown: string): string {
|
|
156
|
+
const outPath = join("dist", `${opName}-report.md`);
|
|
157
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
158
|
+
writeFileSync(outPath, markdown, "utf-8");
|
|
159
|
+
return outPath;
|
|
160
|
+
}
|