@gobing-ai/knowledge-kit 0.0.8 → 0.0.10
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/index.js +102 -50
- package/package.json +4 -4
- package/plugins/generations/dailynews-gen/package.json +17 -0
- package/plugins/generations/dailynews-gen/plugin.json +7 -0
- package/plugins/generations/dailynews-gen/src/index.ts +111 -0
- package/plugins/generations/dailynews-gen/src/script-builder.ts +430 -0
- package/plugins/generations/dailynews-gen/tsconfig.json +4 -0
- package/plugins/generations/voice-gen/src/concat.ts +9 -7
- package/plugins/generations/voice-gen/src/index.ts +65 -11
- package/plugins/generations/voice-gen/src/mp3.ts +65 -0
- package/plugins/generations/voice-gen/src/qc.ts +157 -0
- package/plugins/generations/voice-gen/src/voicebox-client.ts +40 -2
- package/plugins/generations/voice-gen/src/voicescript.ts +149 -12
- package/plugins/ingestions/aihot-ingest/package.json +17 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +7 -0
- package/plugins/ingestions/aihot-ingest/src/client.ts +185 -0
- package/plugins/ingestions/aihot-ingest/src/index.ts +137 -0
- package/plugins/ingestions/aihot-ingest/src/mapper.ts +42 -0
- package/plugins/ingestions/aihot-ingest/tsconfig.json +4 -0
- package/plugins/ingestions/web-search/src/index.ts +2 -1
- package/plugins/kk/commands/workflow-run.md +70 -48
- package/plugins/kk/skills/audio-authoring/SKILL.md +185 -0
- package/plugins/kk/skills/audio-authoring/templates/voicescript.solo.yaml +25 -0
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +6 -7
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +158 -0
- package/plugins/kk/workflows/kk-ingest-generate-publish.yaml +13 -4
- package/plugins/kk/workflows/kk-itc.yaml +14 -18
- package/plugins/kk/workflows/kk-solo-podcast.yaml +135 -104
- package/plugins/kk/workflows/kk-storm-research.yaml +26 -5
- package/plugins/kk/workflows/wrap-voicescript-doc.ts +40 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const AIHOT_BASE_URL = 'https://aihot.virxact.com';
|
|
5
|
+
export const DEFAULT_AIHOT_USER_AGENT = 'aihot-ingest/1.0.0';
|
|
6
|
+
|
|
7
|
+
export const AihotItemSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
title: z.string(),
|
|
10
|
+
originalTitle: z.string().nullable(),
|
|
11
|
+
summary: z.string().nullable(),
|
|
12
|
+
source: z.object({ name: z.string() }),
|
|
13
|
+
links: z.object({
|
|
14
|
+
aihot: z.url(),
|
|
15
|
+
original: z.url(),
|
|
16
|
+
}),
|
|
17
|
+
publishedAt: z.iso.datetime().nullable(),
|
|
18
|
+
discoveredAt: z.iso.datetime(),
|
|
19
|
+
category: z.string().nullable(),
|
|
20
|
+
score: z.number().min(0).max(100).nullable(),
|
|
21
|
+
selected: z.boolean(),
|
|
22
|
+
reason: z.string().nullable().optional(),
|
|
23
|
+
attribution: z
|
|
24
|
+
.object({
|
|
25
|
+
name: z.string(),
|
|
26
|
+
url: z.url(),
|
|
27
|
+
})
|
|
28
|
+
.optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type AihotItem = z.infer<typeof AihotItemSchema>;
|
|
32
|
+
|
|
33
|
+
export const AihotSnapshotResponseSchema = z.object({
|
|
34
|
+
schemaVersion: z.literal(1),
|
|
35
|
+
asOf: z.iso.datetime(),
|
|
36
|
+
fields: z.literal('default'),
|
|
37
|
+
cursor: z.string(),
|
|
38
|
+
count: z.number().int().nonnegative(),
|
|
39
|
+
hasMore: z.boolean(),
|
|
40
|
+
nextPage: z.string().nullable(),
|
|
41
|
+
items: z.array(AihotItemSchema),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type AihotSnapshotResponse = z.infer<typeof AihotSnapshotResponseSchema>;
|
|
45
|
+
|
|
46
|
+
const AihotSnapshotEnvelopeSchema = AihotSnapshotResponseSchema.extend({
|
|
47
|
+
items: z.array(z.unknown()),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const AihotChangeSchema = z.discriminatedUnion('op', [
|
|
51
|
+
z.object({
|
|
52
|
+
op: z.literal('upsert'),
|
|
53
|
+
item: AihotItemSchema,
|
|
54
|
+
changedAt: z.iso.datetime(),
|
|
55
|
+
}),
|
|
56
|
+
z.object({
|
|
57
|
+
op: z.literal('remove'),
|
|
58
|
+
id: z.string(),
|
|
59
|
+
changedAt: z.iso.datetime(),
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export const AihotChangesResponseSchema = z.object({
|
|
64
|
+
schemaVersion: z.literal(1),
|
|
65
|
+
fields: z.literal('default'),
|
|
66
|
+
cursor: z.string(),
|
|
67
|
+
count: z.number().int().nonnegative(),
|
|
68
|
+
hasMore: z.boolean(),
|
|
69
|
+
changes: z.array(AihotChangeSchema),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type AihotChangesResponse = z.infer<typeof AihotChangesResponseSchema>;
|
|
73
|
+
|
|
74
|
+
const AihotChangesEnvelopeSchema = AihotChangesResponseSchema.extend({
|
|
75
|
+
changes: z.array(z.unknown()),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
79
|
+
|
|
80
|
+
export interface AihotDeps {
|
|
81
|
+
fetch: FetchLike;
|
|
82
|
+
baseUrl?: string;
|
|
83
|
+
userAgent?: string;
|
|
84
|
+
warn?: (message: string) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface FetchSnapshotOptions {
|
|
88
|
+
limit?: number;
|
|
89
|
+
page?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface FetchChangesOptions {
|
|
93
|
+
cursor: string;
|
|
94
|
+
limit?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class AihotClient {
|
|
98
|
+
private readonly fetch: FetchLike;
|
|
99
|
+
private readonly baseUrl: string;
|
|
100
|
+
private readonly userAgent: string;
|
|
101
|
+
private readonly warn: (message: string) => void;
|
|
102
|
+
|
|
103
|
+
constructor(deps?: Partial<AihotDeps>) {
|
|
104
|
+
this.fetch = deps?.fetch ?? globalThis.fetch;
|
|
105
|
+
this.baseUrl = (deps?.baseUrl ?? AIHOT_BASE_URL).replace(/\/+$/, '');
|
|
106
|
+
this.userAgent = deps?.userAgent ?? DEFAULT_AIHOT_USER_AGENT;
|
|
107
|
+
this.warn = deps?.warn ?? echoError;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async fetchSnapshot(options?: FetchSnapshotOptions): Promise<AihotSnapshotResponse> {
|
|
111
|
+
const url = new URL(`${this.baseUrl}/api/v1/selected/snapshot`);
|
|
112
|
+
if (options?.limit !== undefined) {
|
|
113
|
+
url.searchParams.set('limit', String(options.limit));
|
|
114
|
+
}
|
|
115
|
+
if (options?.page) {
|
|
116
|
+
url.searchParams.set('page', options.page);
|
|
117
|
+
}
|
|
118
|
+
url.searchParams.set('fields', 'default');
|
|
119
|
+
|
|
120
|
+
const res = await this.fetch(url.toString(), {
|
|
121
|
+
method: 'GET',
|
|
122
|
+
headers: {
|
|
123
|
+
Accept: 'application/json',
|
|
124
|
+
'User-Agent': this.userAgent,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const errBody = (await res.text()).slice(0, 1000);
|
|
130
|
+
throw new Error(
|
|
131
|
+
`aihot snapshot request failed (HTTP ${res.status} ${res.statusText}): ${errBody || 'Unknown error'}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const response = AihotSnapshotEnvelopeSchema.parse(await res.json());
|
|
136
|
+
return {
|
|
137
|
+
...response,
|
|
138
|
+
items: this.parseRecords(response.items, AihotItemSchema, 'snapshot item'),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async fetchChanges(options: FetchChangesOptions): Promise<AihotChangesResponse> {
|
|
143
|
+
const url = new URL(`${this.baseUrl}/api/v1/selected/changes`);
|
|
144
|
+
url.searchParams.set('cursor', options.cursor);
|
|
145
|
+
if (options.limit !== undefined) {
|
|
146
|
+
url.searchParams.set('limit', String(options.limit));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const res = await this.fetch(url.toString(), {
|
|
150
|
+
method: 'GET',
|
|
151
|
+
headers: {
|
|
152
|
+
Accept: 'application/json',
|
|
153
|
+
'User-Agent': this.userAgent,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
const errBody = (await res.text()).slice(0, 1000);
|
|
159
|
+
throw new Error(
|
|
160
|
+
`aihot changes request failed (HTTP ${res.status} ${res.statusText}): ${errBody || 'Unknown error'}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const response = AihotChangesEnvelopeSchema.parse(await res.json());
|
|
165
|
+
return {
|
|
166
|
+
...response,
|
|
167
|
+
changes: this.parseRecords(response.changes, AihotChangeSchema, 'change record'),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private parseRecords<T>(records: unknown[], schema: z.ZodType<T>, label: string): T[] {
|
|
172
|
+
return records.flatMap((record, index) => {
|
|
173
|
+
const result = schema.safeParse(record);
|
|
174
|
+
if (result.success) return [result.data];
|
|
175
|
+
|
|
176
|
+
const details = result.error.issues.map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`);
|
|
177
|
+
this.warn(`aihot: skipping invalid ${label} at index ${index} (${details.join('; ')})`);
|
|
178
|
+
return [];
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createAihotClient(deps?: Partial<AihotDeps>): AihotClient {
|
|
184
|
+
return new AihotClient(deps);
|
|
185
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { type Doc, DocListSchema } from '@gobing-ai/kk-core';
|
|
4
|
+
import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
|
|
5
|
+
import { echoError } from '@gobing-ai/ts-utils';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { type AihotDeps, createAihotClient } from './client';
|
|
8
|
+
import { mapAihotItemsToDocs } from './mapper';
|
|
9
|
+
|
|
10
|
+
export * from './client';
|
|
11
|
+
export * from './mapper';
|
|
12
|
+
|
|
13
|
+
export interface IngestionOptions {
|
|
14
|
+
out: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
cursor?: string;
|
|
17
|
+
stateFile?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IngestionResult {
|
|
21
|
+
docs: Doc[];
|
|
22
|
+
cursor: string;
|
|
23
|
+
asOf?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const IngestionInputSchema = z.object({
|
|
27
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
28
|
+
cursor: z.string().trim().min(1).optional(),
|
|
29
|
+
stateFile: z.string().trim().min(1).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const CursorStateSchema = z.object({ cursor: z.string().trim().min(1) });
|
|
33
|
+
|
|
34
|
+
function parseLimit(value: string): number {
|
|
35
|
+
return z.coerce.number().int().min(1).max(100).parse(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function processIngestionIO(
|
|
39
|
+
options: IngestionOptions,
|
|
40
|
+
depsOverride?: Partial<AihotDeps>,
|
|
41
|
+
): Promise<IngestionResult> {
|
|
42
|
+
const fs = createNodeFileSystem();
|
|
43
|
+
const client = createAihotClient(depsOverride);
|
|
44
|
+
|
|
45
|
+
let effectiveCursor = options.cursor?.trim();
|
|
46
|
+
if (!effectiveCursor && options.stateFile && (await fs.exists(options.stateFile))) {
|
|
47
|
+
effectiveCursor = CursorStateSchema.parse(await readJsonFile(options.stateFile, fs)).cursor;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let items: import('./client').AihotItem[];
|
|
51
|
+
let cursor: string;
|
|
52
|
+
let asOf: string | undefined;
|
|
53
|
+
|
|
54
|
+
if (effectiveCursor) {
|
|
55
|
+
const changes = await client.fetchChanges({
|
|
56
|
+
cursor: effectiveCursor,
|
|
57
|
+
limit: options.limit,
|
|
58
|
+
});
|
|
59
|
+
items = changes.changes.flatMap((change) => (change.op === 'upsert' ? [change.item] : []));
|
|
60
|
+
cursor = changes.cursor;
|
|
61
|
+
} else {
|
|
62
|
+
const snapshot = await client.fetchSnapshot({
|
|
63
|
+
limit: options.limit,
|
|
64
|
+
});
|
|
65
|
+
items = snapshot.items;
|
|
66
|
+
cursor = snapshot.cursor;
|
|
67
|
+
asOf = snapshot.asOf;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const docs = DocListSchema.parse(mapAihotItemsToDocs(items));
|
|
71
|
+
|
|
72
|
+
const outDir = dirname(options.out);
|
|
73
|
+
if (outDir && outDir !== '.') {
|
|
74
|
+
await fs.ensureDir(outDir);
|
|
75
|
+
}
|
|
76
|
+
await atomicWriteJson(options.out, docs, fs);
|
|
77
|
+
|
|
78
|
+
if (options.stateFile) {
|
|
79
|
+
const stateDir = dirname(options.stateFile);
|
|
80
|
+
if (stateDir && stateDir !== '.') {
|
|
81
|
+
await fs.ensureDir(stateDir);
|
|
82
|
+
}
|
|
83
|
+
const statePayload = {
|
|
84
|
+
cursor,
|
|
85
|
+
asOf,
|
|
86
|
+
lastSyncAt: new Date().toISOString(),
|
|
87
|
+
itemCount: docs.length,
|
|
88
|
+
};
|
|
89
|
+
await atomicWriteJson(options.stateFile, statePayload, fs);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { docs, cursor, asOf };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function main(depsOverride?: Partial<AihotDeps>): Promise<number> {
|
|
96
|
+
let values: { in?: string; out?: string; limit?: string; cursor?: string; 'state-file'?: string };
|
|
97
|
+
try {
|
|
98
|
+
({ values } = parseArgs({
|
|
99
|
+
options: {
|
|
100
|
+
in: { type: 'string' },
|
|
101
|
+
out: { type: 'string' },
|
|
102
|
+
limit: { type: 'string' },
|
|
103
|
+
cursor: { type: 'string' },
|
|
104
|
+
'state-file': { type: 'string' },
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
} catch (err: unknown) {
|
|
108
|
+
echoError(`aihot-ingest failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!values.out) {
|
|
113
|
+
echoError('aihot-ingest failed: Missing required argument: --out');
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const input = values.in ? IngestionInputSchema.parse(await readJsonFile(values.in)) : {};
|
|
119
|
+
await processIngestionIO(
|
|
120
|
+
{
|
|
121
|
+
out: values.out,
|
|
122
|
+
limit: values.limit ? parseLimit(values.limit) : (input.limit ?? 10),
|
|
123
|
+
cursor: values.cursor ?? input.cursor,
|
|
124
|
+
stateFile: values['state-file'] ?? input.stateFile,
|
|
125
|
+
},
|
|
126
|
+
depsOverride,
|
|
127
|
+
);
|
|
128
|
+
return 0;
|
|
129
|
+
} catch (err: unknown) {
|
|
130
|
+
echoError(`aihot-ingest failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (import.meta.main) {
|
|
136
|
+
process.exit(await main());
|
|
137
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { Doc } from '@gobing-ai/kk-core';
|
|
3
|
+
import type { AihotItem } from './client';
|
|
4
|
+
|
|
5
|
+
export function computeDocId(seed: string): string {
|
|
6
|
+
return createHash('sha256').update(seed).digest('hex').slice(0, 16);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function mapAihotItemToDoc(item: AihotItem): Doc {
|
|
10
|
+
const rawId = item.id.trim();
|
|
11
|
+
const canonicalSource = item.links.original || item.links.aihot || item.title || 'untitled';
|
|
12
|
+
const id = rawId.length > 0 ? rawId : computeDocId(canonicalSource);
|
|
13
|
+
|
|
14
|
+
const sourceUri = item.links?.original || item.links?.aihot || `https://aihot.virxact.com/items/${id}`;
|
|
15
|
+
const title = item.title.trim() || 'Untitled AI News';
|
|
16
|
+
const body = `${item.summary?.trim() || item.title.trim()}\n`;
|
|
17
|
+
|
|
18
|
+
const metadata: Record<string, unknown> = {
|
|
19
|
+
sourceName: item.source.name,
|
|
20
|
+
publishedAt: item.publishedAt,
|
|
21
|
+
discoveredAt: item.discoveredAt,
|
|
22
|
+
category: item.category ?? undefined,
|
|
23
|
+
score: item.score ?? undefined,
|
|
24
|
+
aihotUrl: item.links.aihot,
|
|
25
|
+
originalUrl: item.links.original,
|
|
26
|
+
attribution: item.attribution,
|
|
27
|
+
reason: item.reason ?? undefined,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
title,
|
|
33
|
+
sourceUri,
|
|
34
|
+
body,
|
|
35
|
+
mediaType: 'text/markdown',
|
|
36
|
+
metadata,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mapAihotItemsToDocs(items: AihotItem[]): Doc[] {
|
|
41
|
+
return items.map(mapAihotItemToDoc);
|
|
42
|
+
}
|
|
@@ -356,7 +356,7 @@ export function parseInput(raw: string): InInput {
|
|
|
356
356
|
return { topic: input.topic, maxResults, fixture: input.fixture === true };
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
export async function main(): Promise<number> {
|
|
359
|
+
export async function main(depsOverride?: Partial<SearchDeps>): Promise<number> {
|
|
360
360
|
const fs = createNodeFileSystem();
|
|
361
361
|
const { values } = parseArgs({
|
|
362
362
|
options: {
|
|
@@ -385,6 +385,7 @@ export async function main(): Promise<number> {
|
|
|
385
385
|
fetch: globalThis.fetch,
|
|
386
386
|
env: process.env,
|
|
387
387
|
now: () => new Date(),
|
|
388
|
+
...depsOverride,
|
|
388
389
|
};
|
|
389
390
|
// Bun.which with an explicit PATH — without the option it can ignore runtime
|
|
390
391
|
// process.env.PATH changes (observed on Bun 1.3.14).
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: workflow-run
|
|
3
3
|
description: >-
|
|
4
|
-
Run STORM daily research
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Run STORM daily research, IT content authoring, or solo-podcast end to end.
|
|
5
|
+
Resolves the workflow YAML from the user override, KK_WORKFLOWS_SOURCE, the
|
|
6
|
+
installed kk package root, or a repo checkout — runs in place from any cwd;
|
|
7
|
+
creates the config with defaults if missing; computes the workspace; shells
|
|
8
|
+
spur workflow run against the resolved YAML.
|
|
7
9
|
argument-hint: "[name] <topic|--in file> [--dir <path>] [--playbook generic|english|wechat] [--research] [--judge] [--outline <a|b|c>] [--writer itc-generating|topic] [--duration <min>] [--language <code>] [--script-approved] [--fixture] [--force]"
|
|
8
10
|
---
|
|
9
11
|
|
|
10
12
|
Thin runner around the **storm-research**, **itc-generating** / **topic**, and **kk-solo-podcast**
|
|
11
13
|
capabilities. Read `plugins/kk/skills/storm-research/SKILL.md` or
|
|
12
|
-
`plugins/kk/skills/itc-generating/SKILL.md` for craft; solo-podcast craft
|
|
13
|
-
|
|
14
|
-
YAML
|
|
15
|
-
|
|
14
|
+
`plugins/kk/skills/itc-generating/SKILL.md` for craft; solo-podcast craft lives in the
|
|
15
|
+
`audio-authoring` skill. This command owns the run procedure: config create,
|
|
16
|
+
workflow YAML resolution (user override → `$KK_WORKFLOWS_SOURCE` → **installed package
|
|
17
|
+
root** → repo checkout, runs in place from any cwd), workspace computation, and the
|
|
18
|
+
`spur workflow run` of the resolved YAML. Design-time SSOT remains
|
|
16
19
|
`plugins/kk/workflows/` in the package/repo. No new `kk` CLI noun (ADR-011) — everything here
|
|
17
20
|
is shell + the workflow.
|
|
18
21
|
|
|
@@ -75,46 +78,58 @@ EOF
|
|
|
75
78
|
`--fixture` sets `fixture=true` regardless of the file. `FIRECRAWL_API_KEY` is **never** read
|
|
76
79
|
from config — env-only.
|
|
77
80
|
|
|
78
|
-
## 3.
|
|
81
|
+
## 3. Resolve the workflow YAML (run anywhere)
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
The workflow YAML runs **in place** — no copy on the happy path. Resolve the `kk`
|
|
84
|
+
package root from the binary itself (works under bun/npm/nvm/homebrew prefixes):
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
```bash
|
|
87
|
+
kbin="$(command -v kk 2>/dev/null || true)"
|
|
88
|
+
while [ -L "$kbin" ]; do
|
|
89
|
+
link="$(readlink "$kbin")"
|
|
90
|
+
case "$link" in /*) kbin="$link";; *) kbin="$(dirname "$kbin")/$link";; esac
|
|
91
|
+
done
|
|
92
|
+
pkg=""; [ -n "$kbin" ] && pkg="$(cd "$(dirname "$kbin")/.." 2>/dev/null && pwd || true)"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Resolution order — first hit wins:
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
1. **User override** `$workflows_dir/$NAME.yaml` (default `~/.config/kk/workflows/`) —
|
|
98
|
+
when it exists it always wins, and nothing is copied.
|
|
99
|
+
2. `$KK_WORKFLOWS_SOURCE/$NAME.yaml`
|
|
100
|
+
3. `$pkg/plugins/kk/workflows/$NAME.yaml` — the installed package copy
|
|
101
|
+
4. `plugins/kk/workflows/$NAME.yaml` — repo checkout (development)
|
|
90
102
|
|
|
91
103
|
```bash
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
echo "warning: $dest differs from install source; leaving user copy (pass --force to replace)" >&2
|
|
104
|
+
dest=""
|
|
105
|
+
if [ -f "$workflows_dir/$NAME.yaml" ]; then
|
|
106
|
+
dest="$workflows_dir/$NAME.yaml"
|
|
107
|
+
else
|
|
108
|
+
for d in "$KK_WORKFLOWS_SOURCE" ${pkg:+"$pkg/plugins/kk/workflows"} "plugins/kk/workflows"; do
|
|
109
|
+
[ -n "$d" ] && [ -f "$d/$NAME.yaml" ] && { dest="$d/$NAME.yaml"; break; }
|
|
110
|
+
done
|
|
111
|
+
[ -z "$dest" ] && { echo "no source workflow for $NAME (searched $workflows_dir, KK_WORKFLOWS_SOURCE, kk package root, plugins/kk/workflows)" >&2; exit 1; }
|
|
101
112
|
fi
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
elif ! cmp -s "$sdir/$side" "$ddir/$side"; then
|
|
112
|
-
echo "warning: $ddir/$side differs from install source; leaving user copy (pass --force to replace)" >&2
|
|
113
|
-
fi
|
|
114
|
-
fi
|
|
113
|
+
# --force: refresh the user override from the best available source, then run the override.
|
|
114
|
+
if [ "$FORCE" = true ]; then
|
|
115
|
+
src=""
|
|
116
|
+
for d in "$KK_WORKFLOWS_SOURCE" ${pkg:+"$pkg/plugins/kk/workflows"} "plugins/kk/workflows"; do
|
|
117
|
+
[ -n "$d" ] && [ -f "$d/$NAME.yaml" ] && { src="$d/$NAME.yaml"; break; }
|
|
118
|
+
done
|
|
119
|
+
[ -z "$src" ] && { echo "no source workflow for $NAME to force-install (searched KK_WORKFLOWS_SOURCE, kk package root, plugins/kk/workflows)" >&2; exit 1; }
|
|
120
|
+
install -d "$workflows_dir" && cp "$src" "$workflows_dir/$NAME.yaml"
|
|
121
|
+
dest="$workflows_dir/$NAME.yaml"
|
|
115
122
|
fi
|
|
116
123
|
```
|
|
117
124
|
|
|
125
|
+
- To customize a workflow, copy it to `$workflows_dir/$NAME.yaml` yourself (or use
|
|
126
|
+
`--force` to place a fresh copy there) — that override wins on every later run.
|
|
127
|
+
- No auto-copy, no drift warnings: the package copy is versioned with the CLI and
|
|
128
|
+
never goes stale; overrides are explicit.
|
|
129
|
+
- Sidecars (`validate-voicescript.ts`, `wrap-voicescript-doc.ts`) are **not copied** —
|
|
130
|
+
the workflow's own fallback chain resolves them via `$KK_WORKFLOWS_DIR`, the package
|
|
131
|
+
root, then `$HOME/.config/kk/workflows`.
|
|
132
|
+
|
|
118
133
|
## 4. Compute workspace
|
|
119
134
|
|
|
120
135
|
### Profile `kk-storm-research` (0056 `topicId`)
|
|
@@ -144,11 +159,12 @@ mkdir -p "$work_dir"
|
|
|
144
159
|
`kebab` = lower-case, `[^a-z0-9]+` → `-`, trimmed dashes (no length cap, no sha256 hash).
|
|
145
160
|
`work_dir="${DIR:-./$kebab}"`. Empty derived `kebab` → exit 1.
|
|
146
161
|
|
|
147
|
-
### Profile `kk-solo-podcast`
|
|
162
|
+
### Profile `kk-solo-podcast` / `kk-daily-ai-voice`
|
|
148
163
|
|
|
164
|
+
`date` = today's date in `YYYY-MM-DD` (`$(date +%Y-%m-%d)`).
|
|
149
165
|
`raw` = `TOPIC` or file-mode first ATX H1 else basename without extension.
|
|
150
166
|
`kebab` = lower-case, `[^a-z0-9]+` → `-`, trimmed dashes (no length cap, no sha256 hash).
|
|
151
|
-
`work_dir="${DIR:-$works_dir/$
|
|
167
|
+
`work_dir="${DIR:-$works_dir/kk-solo-podcast/$date}"`.
|
|
152
168
|
|
|
153
169
|
## 5. Run the workflow
|
|
154
170
|
|
|
@@ -157,8 +173,9 @@ All values are strings.
|
|
|
157
173
|
### Profile `kk-storm-research`
|
|
158
174
|
|
|
159
175
|
Bind vars: `maxResults` and `fixture` from step 2; `plugins_path` defaults to `./plugins`
|
|
160
|
-
in a checkout, else
|
|
161
|
-
`plugins/kk/scripts/render-md.ts
|
|
176
|
+
in a checkout, else `$pkg/plugins` (package root from step 3) or `KK_PLUGIN_PATH`;
|
|
177
|
+
`render_script` is `plugins/kk/scripts/render-md.ts` in a checkout, else
|
|
178
|
+
`$pkg/plugins/kk/scripts/render-md.ts`.
|
|
162
179
|
|
|
163
180
|
```bash
|
|
164
181
|
spur workflow run "$dest" --vars \
|
|
@@ -168,24 +185,29 @@ spur workflow run "$dest" --vars \
|
|
|
168
185
|
### Profile `kk-itc`
|
|
169
186
|
|
|
170
187
|
Bind vars: `topic`, `input_file`, `work_dir`, `writer`, `playbook`, `research`, `judge`, `outline`,
|
|
171
|
-
`force`, `rubric` (`tech-accuracy`), `
|
|
188
|
+
`force`, `rubric` (`tech-accuracy`), `agent`. The judge verdict lands at `$work_dir/.itc-verdict.json`
|
|
189
|
+
(fixed path inside the workflow).
|
|
172
190
|
|
|
173
191
|
```bash
|
|
174
192
|
spur workflow run "$dest" --vars \
|
|
175
|
-
"{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"work_dir\":\"$work_dir\",\"writer\":\"$WRITER\",\"playbook\":\"$PLAYBOOK\",\"research\":\"$RESEARCH\",\"judge\":\"$JUDGE\",\"outline\":\"$OUTLINE\",\"force\":\"$FORCE\",\"rubric\":\"tech-accuracy\",\"
|
|
193
|
+
"{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"work_dir\":\"$work_dir\",\"writer\":\"$WRITER\",\"playbook\":\"$PLAYBOOK\",\"research\":\"$RESEARCH\",\"judge\":\"$JUDGE\",\"outline\":\"$OUTLINE\",\"force\":\"$FORCE\",\"rubric\":\"tech-accuracy\",\"agent\":\"$AGENT\"}"
|
|
176
194
|
```
|
|
177
195
|
|
|
178
196
|
### Profile `kk-solo-podcast`
|
|
179
197
|
|
|
180
198
|
Bind vars: `topic`, `input_file`, `work_dir`, `outline`, `script_approved`, `force`,
|
|
181
199
|
`target_duration_min`, `language`, `voice_profile` (from `VOICEBOX_DEFAULT_PROFILE` or empty),
|
|
182
|
-
`validate_script` (`$workflows_dir/validate-voicescript.ts`
|
|
183
|
-
|
|
184
|
-
|
|
200
|
+
`validate_script` (`$workflows_dir/validate-voicescript.ts` when that override exists, else
|
|
201
|
+
empty), `wrap_script` (`$workflows_dir/wrap-voicescript-doc.ts` when that override exists,
|
|
202
|
+
else empty) — empty binds let the workflow's fallback chain resolve via the package root.
|
|
203
|
+
`plugins_path` (empty → ADR-012 default discovery), `agent`. `$dest` is the user override
|
|
204
|
+
when present, else the package/checkout YAML resolved in step 3 — never a hardcoded path.
|
|
185
205
|
|
|
186
206
|
```bash
|
|
207
|
+
vs_arg=""; [ -f "$workflows_dir/validate-voicescript.ts" ] && vs_arg="$workflows_dir/validate-voicescript.ts"
|
|
208
|
+
ws_arg=""; [ -f "$workflows_dir/wrap-voicescript-doc.ts" ] && ws_arg="$workflows_dir/wrap-voicescript-doc.ts"
|
|
187
209
|
spur workflow run "$dest" --vars \
|
|
188
|
-
"{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"work_dir\":\"$work_dir\",\"outline\":\"$OUTLINE\",\"script_approved\":\"$SCRIPT_APPROVED\",\"force\":\"$FORCE\",\"target_duration_min\":\"$DURATION\",\"language\":\"$LANGUAGE\",\"voice_profile\":\"$VOICE_PROFILE\",\"validate_script\":\"$
|
|
210
|
+
"{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"work_dir\":\"$work_dir\",\"outline\":\"$OUTLINE\",\"script_approved\":\"$SCRIPT_APPROVED\",\"force\":\"$FORCE\",\"target_duration_min\":\"$DURATION\",\"language\":\"$LANGUAGE\",\"voice_profile\":\"$VOICE_PROFILE\",\"validate_script\":\"$vs_arg\",\"wrap_script\":\"$ws_arg\",\"plugins_path\":\"\",\"agent\":\"$AGENT\"}"
|
|
189
211
|
```
|
|
190
212
|
|
|
191
213
|
## 6. Report
|