vidpipe 1.3.16 → 1.3.17

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/cli.js CHANGED
@@ -7904,10 +7904,14 @@ var init_CopilotProvider = __esm({
7904
7904
  let response;
7905
7905
  let sdkError;
7906
7906
  try {
7907
- response = await this.session.sendAndWait(
7908
- { prompt: message },
7909
- this.timeoutMs
7910
- );
7907
+ if (this.timeoutMs === 0) {
7908
+ response = await this.sendAndWaitForIdle(message);
7909
+ } else {
7910
+ response = await this.session.sendAndWait(
7911
+ { prompt: message },
7912
+ this.timeoutMs
7913
+ );
7914
+ }
7911
7915
  } catch (err) {
7912
7916
  sdkError = err instanceof Error ? err : new Error(String(err));
7913
7917
  if (sdkError.message.includes("missing finish_reason")) {
@@ -7931,6 +7935,38 @@ var init_CopilotProvider = __esm({
7931
7935
  durationMs: Date.now() - start
7932
7936
  };
7933
7937
  }
7938
+ /**
7939
+ * Send a message and wait for session.idle without any timeout.
7940
+ * Used by interactive agents (interview, chat) where tool handlers
7941
+ * block waiting for human input — the SDK's sendAndWait() timeout
7942
+ * would fire while the agent is legitimately waiting for the user.
7943
+ */
7944
+ sendAndWaitForIdle(message) {
7945
+ return new Promise((resolve3, reject) => {
7946
+ let lastAssistantMessage;
7947
+ const unsubMessage = this.session.on("assistant.message", (event) => {
7948
+ lastAssistantMessage = event;
7949
+ });
7950
+ const unsubIdle = this.session.on("session.idle", () => {
7951
+ unsubMessage();
7952
+ unsubIdle();
7953
+ unsubError();
7954
+ resolve3(lastAssistantMessage);
7955
+ });
7956
+ const unsubError = this.session.on("session.error", (event) => {
7957
+ unsubMessage();
7958
+ unsubIdle();
7959
+ unsubError();
7960
+ reject(new Error(event.data?.message ?? "Unknown session error"));
7961
+ });
7962
+ this.session.send({ prompt: message }).catch((err) => {
7963
+ unsubMessage();
7964
+ unsubIdle();
7965
+ unsubError();
7966
+ reject(err instanceof Error ? err : new Error(String(err)));
7967
+ });
7968
+ });
7969
+ }
7934
7970
  on(event, handler) {
7935
7971
  const handlers = this.eventHandlers.get(event) ?? [];
7936
7972
  handlers.push(handler);
@@ -18812,6 +18848,352 @@ async function generateIdeas(options = {}) {
18812
18848
  }
18813
18849
  }
18814
18850
 
18851
+ // src/L4-agents/InterviewAgent.ts
18852
+ init_BaseAgent();
18853
+
18854
+ // src/L1-infra/progress/interviewEmitter.ts
18855
+ var InterviewEmitter = class {
18856
+ enabled = false;
18857
+ listeners = /* @__PURE__ */ new Set();
18858
+ /** Turn on interview event output to stderr. */
18859
+ enable() {
18860
+ this.enabled = true;
18861
+ }
18862
+ /** Turn off interview event output. */
18863
+ disable() {
18864
+ this.enabled = false;
18865
+ }
18866
+ /** Whether the emitter is currently active (stderr or listeners). */
18867
+ isEnabled() {
18868
+ return this.enabled || this.listeners.size > 0;
18869
+ }
18870
+ /** Register a programmatic listener for interview events. */
18871
+ addListener(fn) {
18872
+ this.listeners.add(fn);
18873
+ }
18874
+ /** Remove a previously registered listener. */
18875
+ removeListener(fn) {
18876
+ this.listeners.delete(fn);
18877
+ }
18878
+ /**
18879
+ * Write an interview event as a single JSON line to stderr (if enabled)
18880
+ * and dispatch to all registered listeners.
18881
+ * No-op when neither stderr output nor listeners are active.
18882
+ */
18883
+ emit(event) {
18884
+ if (!this.enabled && this.listeners.size === 0) return;
18885
+ if (this.enabled) {
18886
+ process.stderr.write(JSON.stringify(event) + "\n");
18887
+ }
18888
+ for (const listener of this.listeners) {
18889
+ listener(event);
18890
+ }
18891
+ }
18892
+ };
18893
+ var interviewEmitter = new InterviewEmitter();
18894
+
18895
+ // src/L4-agents/InterviewAgent.ts
18896
+ init_configLogger();
18897
+ var SYSTEM_PROMPT8 = `You are a Socratic interview coach helping a content creator sharpen their video idea. Ask ONE short question at a time (1 sentence max).
18898
+
18899
+ ## Rules
18900
+ - Every question must be a SINGLE sentence. No multi-part questions. No preamble. No encouragement filler.
18901
+ - Build on the previous answer \u2014 reference what the user said.
18902
+ - Push on weak spots: vague audience, generic hooks, surface-level talking points.
18903
+ - If the user responds with "/end", call end_interview immediately.
18904
+
18905
+ ## Focus (pick one per question)
18906
+ - Problem clarity \u2014 what specific pain does this solve?
18907
+ - Audience \u2014 who exactly, what skill level?
18908
+ - Key takeaway \u2014 what's the ONE thing to remember?
18909
+ - Hook \u2014 would you click this? Be specific.
18910
+ - Talking points \u2014 substantive or surface-level?
18911
+ - Trend relevance \u2014 why now?
18912
+
18913
+ ## Tools
18914
+ - ask_question: EVERY question goes through this tool. Include a 1-sentence rationale and the target field.
18915
+ - update_field: When the conversation reveals a better value for a field, DIRECTLY SET the new value. For scalar fields (hook, audience, keyTakeaway, trendContext), provide the complete replacement text. For array fields (talkingPoints), provide the FULL updated list \u2014 not just the new item. Write the actual content, not a description of the change.
18916
+ - end_interview: After 5\u201310 productive questions, wrap up with a brief summary.
18917
+ - NEVER output text outside of tool calls.`;
18918
+ var InterviewAgent = class extends BaseAgent {
18919
+ answerProvider = null;
18920
+ transcript = [];
18921
+ insights = {};
18922
+ questionNumber = 0;
18923
+ ended = false;
18924
+ idea = null;
18925
+ constructor(model) {
18926
+ super("InterviewAgent", SYSTEM_PROMPT8, void 0, model);
18927
+ }
18928
+ getTimeoutMs() {
18929
+ return 0;
18930
+ }
18931
+ resetForRetry() {
18932
+ this.transcript = [];
18933
+ this.insights = {};
18934
+ this.questionNumber = 0;
18935
+ this.ended = false;
18936
+ }
18937
+ getTools() {
18938
+ return [
18939
+ {
18940
+ name: "ask_question",
18941
+ description: "Ask the user a single Socratic question to explore and develop the idea. This is the primary way you communicate \u2014 every question MUST go through this tool.",
18942
+ parameters: {
18943
+ type: "object",
18944
+ properties: {
18945
+ question: {
18946
+ type: "string",
18947
+ description: "The question to ask the user. Must be a single, focused question."
18948
+ },
18949
+ rationale: {
18950
+ type: "string",
18951
+ description: "Why you are asking this question \u2014 what gap or opportunity it explores."
18952
+ },
18953
+ targetField: {
18954
+ type: "string",
18955
+ description: "Which idea field this question explores (e.g. hook, audience, keyTakeaway, talkingPoints, trendContext).",
18956
+ enum: ["topic", "hook", "audience", "keyTakeaway", "talkingPoints", "platforms", "tags", "publishBy", "trendContext"]
18957
+ }
18958
+ },
18959
+ required: ["question", "rationale"],
18960
+ additionalProperties: false
18961
+ },
18962
+ handler: async (args) => this.handleToolCall("ask_question", args)
18963
+ },
18964
+ {
18965
+ name: "update_field",
18966
+ description: "Directly update an idea field with new content discovered during the interview. For scalar fields, provide the complete replacement text. For talkingPoints, provide the FULL updated list (all points, not just new ones).",
18967
+ parameters: {
18968
+ type: "object",
18969
+ properties: {
18970
+ field: {
18971
+ type: "string",
18972
+ description: "Which idea field to update.",
18973
+ enum: ["topic", "hook", "audience", "keyTakeaway", "talkingPoints", "tags", "trendContext"]
18974
+ },
18975
+ value: {
18976
+ type: "string",
18977
+ description: "The new value for scalar fields (hook, audience, keyTakeaway, trendContext, topic)."
18978
+ },
18979
+ values: {
18980
+ type: "array",
18981
+ items: { type: "string" },
18982
+ description: "The full updated list for array fields (talkingPoints, tags). Include ALL items, not just new ones."
18983
+ }
18984
+ },
18985
+ required: ["field"],
18986
+ additionalProperties: false
18987
+ },
18988
+ handler: async (args) => this.handleToolCall("update_field", args)
18989
+ },
18990
+ {
18991
+ name: "end_interview",
18992
+ description: "Signal that the interview is complete. Use when you have gathered sufficient insights (typically after 5\u201310 questions) to meaningfully improve the idea.",
18993
+ parameters: {
18994
+ type: "object",
18995
+ properties: {
18996
+ summary: {
18997
+ type: "string",
18998
+ description: "A summary of what was learned and how the idea has been refined."
18999
+ }
19000
+ },
19001
+ required: ["summary"],
19002
+ additionalProperties: false
19003
+ },
19004
+ handler: async (args) => this.handleToolCall("end_interview", args)
19005
+ }
19006
+ ];
19007
+ }
19008
+ async handleToolCall(toolName, args) {
19009
+ switch (toolName) {
19010
+ case "ask_question":
19011
+ return this.handleAskQuestion(args);
19012
+ case "update_field":
19013
+ return this.handleUpdateField(args);
19014
+ case "end_interview":
19015
+ return this.handleEndInterview(args);
19016
+ default:
19017
+ return { error: `Unknown tool: ${toolName}` };
19018
+ }
19019
+ }
19020
+ async handleAskQuestion(args) {
19021
+ const question = String(args.question ?? "");
19022
+ const rationale = String(args.rationale ?? "");
19023
+ const targetField = args.targetField;
19024
+ this.questionNumber++;
19025
+ const context = {
19026
+ rationale,
19027
+ targetField,
19028
+ questionNumber: this.questionNumber
19029
+ };
19030
+ interviewEmitter.emit({
19031
+ event: "question:asked",
19032
+ question,
19033
+ context,
19034
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19035
+ });
19036
+ logger_default.info(`[InterviewAgent] Q${this.questionNumber}: ${question}`);
19037
+ return this.waitForAnswer(question, context);
19038
+ }
19039
+ handleUpdateField(args) {
19040
+ const field = String(args.field ?? "");
19041
+ if (field === "talkingPoints" || field === "tags") {
19042
+ const values = args.values;
19043
+ if (values && values.length > 0) {
19044
+ this.insights[field] = values;
19045
+ }
19046
+ } else {
19047
+ const value = String(args.value ?? "");
19048
+ if (value) {
19049
+ this.insights[field] = value;
19050
+ }
19051
+ }
19052
+ const displayValue = field === "talkingPoints" || field === "tags" ? `[${(args.values ?? []).length} items]` : String(args.value ?? "").slice(0, 60);
19053
+ interviewEmitter.emit({
19054
+ event: "insight:discovered",
19055
+ insight: displayValue,
19056
+ field,
19057
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19058
+ });
19059
+ logger_default.info(`[InterviewAgent] Updated [${field}]: ${displayValue}`);
19060
+ return { updated: true, field };
19061
+ }
19062
+ handleEndInterview(args) {
19063
+ const summary = String(args.summary ?? "");
19064
+ this.ended = true;
19065
+ logger_default.info(`[InterviewAgent] Interview ended: ${summary}`);
19066
+ return { ended: true, summary };
19067
+ }
19068
+ async waitForAnswer(question, context) {
19069
+ if (!this.answerProvider) {
19070
+ throw new Error("No answer provider configured \u2014 cannot ask questions");
19071
+ }
19072
+ const askedAt = (/* @__PURE__ */ new Date()).toISOString();
19073
+ const answer = await this.answerProvider(question, context);
19074
+ const answeredAt = (/* @__PURE__ */ new Date()).toISOString();
19075
+ const pair = {
19076
+ question,
19077
+ answer,
19078
+ askedAt,
19079
+ answeredAt,
19080
+ questionNumber: context.questionNumber
19081
+ };
19082
+ this.transcript.push(pair);
19083
+ interviewEmitter.emit({
19084
+ event: "answer:received",
19085
+ questionNumber: context.questionNumber,
19086
+ answer,
19087
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19088
+ });
19089
+ return answer;
19090
+ }
19091
+ /**
19092
+ * Run a Socratic interview session for the given idea.
19093
+ *
19094
+ * The agent uses `ask_question` tool calls to present questions one at a time.
19095
+ * Each question is routed through the `answerProvider` callback, which the caller
19096
+ * implements to show the question to the user and collect their response.
19097
+ */
19098
+ async runInterview(idea, answerProvider) {
19099
+ this.idea = idea;
19100
+ this.answerProvider = answerProvider;
19101
+ this.transcript = [];
19102
+ this.insights = {};
19103
+ this.questionNumber = 0;
19104
+ this.ended = false;
19105
+ const startTime = Date.now();
19106
+ interviewEmitter.emit({
19107
+ event: "interview:start",
19108
+ ideaNumber: idea.issueNumber,
19109
+ mode: "interview",
19110
+ ideaTopic: idea.topic,
19111
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19112
+ });
19113
+ const contextMessage = this.buildIdeaContext(idea);
19114
+ try {
19115
+ await this.run(contextMessage);
19116
+ const result = {
19117
+ ideaNumber: idea.issueNumber,
19118
+ transcript: this.transcript,
19119
+ insights: this.insights,
19120
+ updatedFields: this.getUpdatedFields(),
19121
+ durationMs: Date.now() - startTime,
19122
+ endedBy: this.ended ? "agent" : "user"
19123
+ };
19124
+ interviewEmitter.emit({
19125
+ event: "interview:complete",
19126
+ result,
19127
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19128
+ });
19129
+ return result;
19130
+ } catch (error) {
19131
+ const message = error instanceof Error ? error.message : String(error);
19132
+ interviewEmitter.emit({
19133
+ event: "interview:error",
19134
+ error: message,
19135
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19136
+ });
19137
+ throw error;
19138
+ }
19139
+ }
19140
+ buildIdeaContext(idea) {
19141
+ const talkingPoints = idea.talkingPoints.length > 0 ? idea.talkingPoints.map((p) => `- ${p}`).join("\n") : "- (none yet)";
19142
+ return [
19143
+ "Here is the idea to explore through Socratic questioning:",
19144
+ "",
19145
+ `**Topic:** ${idea.topic}`,
19146
+ `**Hook:** ${idea.hook}`,
19147
+ `**Audience:** ${idea.audience}`,
19148
+ `**Key Takeaway:** ${idea.keyTakeaway}`,
19149
+ "**Talking Points:**",
19150
+ talkingPoints,
19151
+ `**Publish By:** ${idea.publishBy}`,
19152
+ `**Trend Context:** ${idea.trendContext ?? "Not specified"}`,
19153
+ "",
19154
+ "Begin by asking your first Socratic question to explore and develop this idea."
19155
+ ].join("\n");
19156
+ }
19157
+ getUpdatedFields() {
19158
+ const fields = [];
19159
+ if (this.insights.talkingPoints !== void 0) fields.push("talkingPoints");
19160
+ if (this.insights.keyTakeaway !== void 0) fields.push("keyTakeaway");
19161
+ if (this.insights.hook !== void 0) fields.push("hook");
19162
+ if (this.insights.audience !== void 0) fields.push("audience");
19163
+ if (this.insights.trendContext !== void 0) fields.push("trendContext");
19164
+ if (this.insights.tags !== void 0) fields.push("tags");
19165
+ return fields;
19166
+ }
19167
+ setupEventHandlers(session) {
19168
+ session.on("delta", () => {
19169
+ });
19170
+ session.on("tool_start", (event) => {
19171
+ const toolName = event.data?.name ?? "unknown";
19172
+ if (toolName !== "ask_question") {
19173
+ interviewEmitter.emit({
19174
+ event: "tool:start",
19175
+ toolName,
19176
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19177
+ });
19178
+ }
19179
+ });
19180
+ session.on("tool_end", (event) => {
19181
+ const toolName = event.data?.name ?? "unknown";
19182
+ if (toolName !== "ask_question") {
19183
+ interviewEmitter.emit({
19184
+ event: "tool:end",
19185
+ toolName,
19186
+ durationMs: 0,
19187
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19188
+ });
19189
+ }
19190
+ });
19191
+ session.on("error", (event) => {
19192
+ logger_default.error(`[InterviewAgent] error: ${JSON.stringify(event.data)}`);
19193
+ });
19194
+ }
19195
+ };
19196
+
18815
19197
  // src/L5-assets/pipelineServices.ts
18816
19198
  var costTracker3 = {
18817
19199
  reset: (...args) => costTracker2.reset(...args),
@@ -18835,6 +19217,9 @@ function markFailed3(...args) {
18835
19217
  function generateIdeas2(...args) {
18836
19218
  return generateIdeas(...args);
18837
19219
  }
19220
+ function createInterviewAgent(...args) {
19221
+ return new InterviewAgent(...args);
19222
+ }
18838
19223
  function createScheduleAgent(...args) {
18839
19224
  return new ScheduleAgent(...args);
18840
19225
  }
@@ -19789,15 +20174,225 @@ async function runRealign(options = {}) {
19789
20174
  init_environment();
19790
20175
  init_configLogger();
19791
20176
 
19792
- // src/L1-infra/readline/readline.ts
19793
- import { createInterface } from "readline";
19794
- function createChatInterface(options) {
19795
- return createInterface({
19796
- input: options?.input ?? process.stdin,
19797
- output: options?.output ?? process.stdout,
19798
- terminal: false
20177
+ // src/L1-infra/terminal/altScreenChat.tsx
20178
+ import { useState, useCallback, useEffect } from "react";
20179
+ import { render, Box, Text, useInput, useApp, useStdout } from "ink";
20180
+ import TextInput from "ink-text-input";
20181
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
20182
+ function Header({ title, subtitle }) {
20183
+ const { stdout } = useStdout();
20184
+ const cols = stdout?.columns ?? 80;
20185
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: cols, children: [
20186
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { backgroundColor: "blue", color: "white", bold: true, children: ` \u{1F4DD} ${title}`.padEnd(cols) }) }),
20187
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { backgroundColor: "blue", color: "white", dimColor: true, children: ` ${subtitle}`.padEnd(cols) }) }),
20188
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(cols) })
20189
+ ] });
20190
+ }
20191
+ var FIELD_EMOJI = {
20192
+ topic: "\u{1F4CC} topic",
20193
+ hook: "\u{1FA9D} hook",
20194
+ audience: "\u{1F3AF} audience",
20195
+ keyTakeaway: "\u{1F48E} takeaway",
20196
+ talkingPoints: "\u{1F4CB} talking points",
20197
+ platforms: "\u{1F4F1} platforms",
20198
+ tags: "\u{1F3F7}\uFE0F tags",
20199
+ publishBy: "\u{1F4C5} deadline",
20200
+ trendContext: "\u{1F525} trend"
20201
+ };
20202
+ function QuestionCardView({ card, latestInsight, statusText }) {
20203
+ if (!card) {
20204
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: statusText || "Preparing first question..." }) });
20205
+ }
20206
+ const fieldLabel = FIELD_EMOJI[card.targetField] ?? `\u{1F4CE} ${card.targetField}`;
20207
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 2, children: [
20208
+ /* @__PURE__ */ jsx(Text, { children: " " }),
20209
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
20210
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
20211
+ "Question ",
20212
+ card.questionNumber
20213
+ ] }),
20214
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: fieldLabel })
20215
+ ] }),
20216
+ /* @__PURE__ */ jsx(Text, { children: " " }),
20217
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, wrap: "wrap", children: card.question }),
20218
+ /* @__PURE__ */ jsx(Text, { children: " " }),
20219
+ /* @__PURE__ */ jsxs(Box, { children: [
20220
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u{1F4AD} " }),
20221
+ /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: card.rationale })
20222
+ ] }),
20223
+ latestInsight && /* @__PURE__ */ jsxs(Fragment, { children: [
20224
+ /* @__PURE__ */ jsx(Text, { children: " " }),
20225
+ /* @__PURE__ */ jsxs(Box, { children: [
20226
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u{1F4A1} " }),
20227
+ /* @__PURE__ */ jsx(Text, { color: "yellow", wrap: "wrap", children: latestInsight })
20228
+ ] })
20229
+ ] }),
20230
+ /* @__PURE__ */ jsx(Text, { children: " " }),
20231
+ statusText && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: statusText }) })
20232
+ ] });
20233
+ }
20234
+ function InputLine({ prompt, value, onChange, onSubmit, active }) {
20235
+ const { stdout } = useStdout();
20236
+ const cols = stdout?.columns ?? 80;
20237
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
20238
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(cols) }),
20239
+ /* @__PURE__ */ jsxs(Box, { children: [
20240
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: prompt }),
20241
+ active ? /* @__PURE__ */ jsx(TextInput, { value, onChange, onSubmit }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: " waiting..." })
20242
+ ] })
20243
+ ] });
20244
+ }
20245
+ function ChatApp({ controller }) {
20246
+ const { exit } = useApp();
20247
+ const [card, setCard] = useState(null);
20248
+ const [latestInsight, setLatestInsight] = useState(null);
20249
+ const [statusText, setStatusText] = useState("");
20250
+ const [inputValue, setInputValue] = useState("");
20251
+ const [inputActive, setInputActive] = useState(false);
20252
+ const [, setTick] = useState(0);
20253
+ useEffect(() => {
20254
+ controller._wire({
20255
+ setCard,
20256
+ setLatestInsight,
20257
+ setStatusText,
20258
+ setInputActive,
20259
+ exit,
20260
+ forceRender: () => setTick((t) => t + 1)
20261
+ });
20262
+ return () => controller._unwire();
20263
+ }, [controller, exit]);
20264
+ useInput((_input, key) => {
20265
+ if (key.ctrl && _input === "c") {
20266
+ controller.interrupted = true;
20267
+ const resolve3 = controller._pendingResolve;
20268
+ controller._pendingResolve = null;
20269
+ if (resolve3) resolve3("");
20270
+ exit();
20271
+ }
19799
20272
  });
19800
- }
20273
+ const handleSubmit = useCallback((value) => {
20274
+ setInputValue("");
20275
+ setInputActive(false);
20276
+ const resolve3 = controller._pendingResolve;
20277
+ controller._pendingResolve = null;
20278
+ if (resolve3) resolve3(value.trim());
20279
+ }, [controller]);
20280
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: "100%", children: [
20281
+ /* @__PURE__ */ jsx(
20282
+ Header,
20283
+ {
20284
+ title: controller.title,
20285
+ subtitle: controller.subtitle
20286
+ }
20287
+ ),
20288
+ /* @__PURE__ */ jsx(
20289
+ QuestionCardView,
20290
+ {
20291
+ card,
20292
+ latestInsight,
20293
+ statusText
20294
+ }
20295
+ ),
20296
+ /* @__PURE__ */ jsx(
20297
+ InputLine,
20298
+ {
20299
+ prompt: controller.inputPrompt,
20300
+ value: inputValue,
20301
+ onChange: setInputValue,
20302
+ onSubmit: handleSubmit,
20303
+ active: inputActive
20304
+ }
20305
+ )
20306
+ ] });
20307
+ }
20308
+ var AltScreenChat = class {
20309
+ title;
20310
+ subtitle;
20311
+ inputPrompt;
20312
+ maxScrollback;
20313
+ messages = [];
20314
+ bridge = null;
20315
+ inkInstance = null;
20316
+ /** Set to true when Ctrl+C is pressed. Callers should check this after promptInput(). */
20317
+ interrupted = false;
20318
+ /** @internal */
20319
+ _pendingResolve = null;
20320
+ constructor(options) {
20321
+ this.title = options.title;
20322
+ this.subtitle = options.subtitle ?? "Type /end to finish, Ctrl+C to quit";
20323
+ this.inputPrompt = options.inputPrompt ?? "> ";
20324
+ this.maxScrollback = options.maxScrollback ?? 500;
20325
+ }
20326
+ /** @internal */
20327
+ _wire(bridge) {
20328
+ this.bridge = bridge;
20329
+ }
20330
+ /** @internal */
20331
+ _unwire() {
20332
+ this.bridge = null;
20333
+ }
20334
+ /** Enter fullscreen and render the Ink UI. */
20335
+ enter() {
20336
+ this.inkInstance = render(
20337
+ /* @__PURE__ */ jsx(ChatApp, { controller: this }),
20338
+ { exitOnCtrlC: false }
20339
+ );
20340
+ }
20341
+ /** Leave fullscreen and clean up Ink. */
20342
+ leave() {
20343
+ if (this.inkInstance) {
20344
+ this.inkInstance.unmount();
20345
+ this.inkInstance = null;
20346
+ }
20347
+ }
20348
+ /** Clean up everything. */
20349
+ destroy() {
20350
+ this.leave();
20351
+ this.messages = [];
20352
+ this.bridge = null;
20353
+ this._pendingResolve = null;
20354
+ }
20355
+ /**
20356
+ * Show a focused question card. Replaces the entire display content
20357
+ * with this one question — no scrolling chat history.
20358
+ */
20359
+ showQuestion(question, rationale, targetField, questionNumber) {
20360
+ this.bridge?.setCard({ question, rationale, targetField, questionNumber });
20361
+ }
20362
+ /**
20363
+ * Show a discovered insight on the current card.
20364
+ */
20365
+ showInsight(text) {
20366
+ this.bridge?.setLatestInsight(text);
20367
+ }
20368
+ /**
20369
+ * Add a message to the internal log (for transcript purposes).
20370
+ * Does NOT affect the card display — use showQuestion for that.
20371
+ */
20372
+ addMessage(role, content) {
20373
+ const msg = { role, content, timestamp: /* @__PURE__ */ new Date() };
20374
+ this.messages.push(msg);
20375
+ if (this.messages.length > this.maxScrollback) {
20376
+ this.messages = this.messages.slice(-this.maxScrollback);
20377
+ }
20378
+ }
20379
+ /** Set the status bar text. */
20380
+ setStatus(text) {
20381
+ this.bridge?.setStatusText(text);
20382
+ }
20383
+ /** Clear the status bar. */
20384
+ clearStatus() {
20385
+ this.bridge?.setStatusText("");
20386
+ }
20387
+ /** Prompt for user input. Returns their trimmed text. */
20388
+ promptInput(_prompt) {
20389
+ return new Promise((resolve3) => {
20390
+ this._pendingResolve = resolve3;
20391
+ this.bridge?.setInputActive(true);
20392
+ this.bridge?.forceRender();
20393
+ });
20394
+ }
20395
+ };
19801
20396
 
19802
20397
  // src/L6-pipeline/scheduleChat.ts
19803
20398
  function createScheduleAgent2(...args) {
@@ -19808,21 +20403,21 @@ function createScheduleAgent2(...args) {
19808
20403
  async function runChat() {
19809
20404
  initConfig();
19810
20405
  setChatMode(true);
19811
- const rl2 = createChatInterface();
20406
+ const chat = new AltScreenChat({
20407
+ title: "\u{1F4AC} VidPipe Chat",
20408
+ subtitle: "Schedule management assistant. Type exit or quit to leave.",
20409
+ inputPrompt: "vidpipe> "
20410
+ });
19812
20411
  const handleUserInput = (request) => {
20412
+ chat.addMessage("agent", request.question);
20413
+ if (request.choices && request.choices.length > 0) {
20414
+ const choiceText = request.choices.map((c, i) => ` ${i + 1}. ${c}`).join("\n");
20415
+ chat.addMessage("system", choiceText + (request.allowFreeform !== false ? "\n (or type a custom answer)" : ""));
20416
+ }
19813
20417
  return new Promise((resolve3) => {
19814
- console.log();
19815
- console.log(`\x1B[33m\u{1F916} Agent asks:\x1B[0m ${request.question}`);
19816
- if (request.choices && request.choices.length > 0) {
19817
- for (let i = 0; i < request.choices.length; i++) {
19818
- console.log(` ${i + 1}. ${request.choices[i]}`);
19819
- }
19820
- if (request.allowFreeform !== false) {
19821
- console.log(` (or type a custom answer)`);
19822
- }
19823
- }
19824
- rl2.question("\x1B[33m> \x1B[0m", (answer) => {
20418
+ chat.promptInput("> ").then((answer) => {
19825
20419
  const trimmed = answer.trim();
20420
+ chat.addMessage("user", trimmed);
19826
20421
  if (request.choices && request.choices.length > 0) {
19827
20422
  const num = parseInt(trimmed, 10);
19828
20423
  if (num >= 1 && num <= request.choices.length) {
@@ -19836,61 +20431,36 @@ async function runChat() {
19836
20431
  };
19837
20432
  const agent = createScheduleAgent2(handleUserInput);
19838
20433
  agent.setChatOutput((message) => {
19839
- process.stderr.write(`${message}
19840
- `);
19841
- });
19842
- console.log(`
19843
- \x1B[36m\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
19844
- \u2551 VidPipe Chat \u2551
19845
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\x1B[0m
19846
-
19847
- Schedule management assistant. Ask me about your posting schedule,
19848
- reschedule posts, check what's coming up, or reprioritize content.
19849
-
19850
- Type \x1B[33mexit\x1B[0m or \x1B[33mquit\x1B[0m to leave. Press Ctrl+C to stop.
19851
- `);
19852
- let closeRejector = null;
19853
- const closePromise = new Promise((_, reject) => {
19854
- closeRejector = reject;
19855
- rl2.once("close", () => reject(new Error("readline closed")));
20434
+ chat.setStatus(message);
19856
20435
  });
19857
- const prompt = () => {
19858
- return Promise.race([
19859
- new Promise((resolve3) => {
19860
- rl2.question("\x1B[32mvidpipe>\x1B[0m ", (answer) => {
19861
- resolve3(answer);
19862
- });
19863
- }),
19864
- closePromise
19865
- ]);
19866
- };
20436
+ chat.enter();
20437
+ chat.addMessage("system", "Ask me about your posting schedule, reschedule posts, check what's coming up, or reprioritize content.");
19867
20438
  try {
19868
20439
  while (true) {
19869
- let input;
19870
- try {
19871
- input = await prompt();
19872
- } catch {
19873
- break;
19874
- }
20440
+ const input = await chat.promptInput();
19875
20441
  const trimmed = input.trim();
19876
20442
  if (!trimmed) continue;
19877
20443
  if (trimmed === "exit" || trimmed === "quit") {
19878
- console.log("\nGoodbye! \u{1F44B}");
20444
+ chat.addMessage("system", "Goodbye! \u{1F44B}");
19879
20445
  break;
19880
20446
  }
20447
+ chat.addMessage("user", trimmed);
20448
+ chat.setStatus("\u{1F914} Thinking...");
19881
20449
  try {
19882
- await agent.run(trimmed);
19883
- console.log("\n");
20450
+ const response = await agent.run(trimmed);
20451
+ chat.clearStatus();
20452
+ if (response) {
20453
+ chat.addMessage("agent", response);
20454
+ }
19884
20455
  } catch (err) {
20456
+ chat.clearStatus();
19885
20457
  const message = err instanceof Error ? err.message : String(err);
19886
- console.error(`
19887
- \x1B[31mError: ${message}\x1B[0m
19888
- `);
20458
+ chat.addMessage("error", message);
19889
20459
  }
19890
20460
  }
19891
20461
  } finally {
19892
20462
  await agent.destroy();
19893
- rl2.close();
20463
+ chat.destroy();
19894
20464
  setChatMode(false);
19895
20465
  }
19896
20466
  }
@@ -19903,6 +20473,16 @@ init_ideaService();
19903
20473
  function generateIdeas3(...args) {
19904
20474
  return generateIdeas2(...args);
19905
20475
  }
20476
+ async function startInterview(idea, answerProvider, onEvent) {
20477
+ if (onEvent) interviewEmitter.addListener(onEvent);
20478
+ const agent = createInterviewAgent();
20479
+ try {
20480
+ return await agent.runInterview(idea, answerProvider);
20481
+ } finally {
20482
+ await agent.destroy();
20483
+ if (onEvent) interviewEmitter.removeListener(onEvent);
20484
+ }
20485
+ }
19906
20486
 
19907
20487
  // src/L7-app/commands/ideate.ts
19908
20488
  init_types();
@@ -20072,10 +20652,200 @@ async function handleAdd(options) {
20072
20652
  }
20073
20653
  }
20074
20654
 
20655
+ // src/L7-app/commands/ideateStart.ts
20656
+ init_environment();
20657
+ init_configLogger();
20658
+
20659
+ // src/L3-services/interview/interviewService.ts
20660
+ init_configLogger();
20661
+ init_githubClient();
20662
+ init_ideaService();
20663
+ async function loadAndValidateIdea(issueNumber) {
20664
+ const idea = await getIdea(issueNumber);
20665
+ if (!idea) {
20666
+ throw new Error(`Idea #${issueNumber} not found`);
20667
+ }
20668
+ if (idea.status !== "draft") {
20669
+ throw new Error(
20670
+ `Idea #${issueNumber} has status "${idea.status}" \u2014 only draft ideas can be started`
20671
+ );
20672
+ }
20673
+ return idea;
20674
+ }
20675
+ function formatTranscriptComment(transcript) {
20676
+ const now = (/* @__PURE__ */ new Date()).toISOString();
20677
+ const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
20678
+ year: "numeric",
20679
+ month: "long",
20680
+ day: "numeric"
20681
+ });
20682
+ const lines = [
20683
+ "<!-- vidpipe:idea-comment -->",
20684
+ `<!-- {"type":"interview-transcript","savedAt":"${now}"} -->`,
20685
+ "",
20686
+ "## \u{1F399}\uFE0F Interview Transcript",
20687
+ "",
20688
+ `**Questions asked:** ${transcript.length}`,
20689
+ `**Date:** ${date}`,
20690
+ "",
20691
+ "---"
20692
+ ];
20693
+ for (const qa of transcript) {
20694
+ lines.push("");
20695
+ lines.push(`### Q${qa.questionNumber}: ${qa.question}`);
20696
+ lines.push(`> ${qa.answer}`);
20697
+ }
20698
+ return lines.join("\n");
20699
+ }
20700
+ async function saveTranscript(issueNumber, transcript) {
20701
+ const body = formatTranscriptComment(transcript);
20702
+ const client = getGitHubClient();
20703
+ await client.addComment(issueNumber, body);
20704
+ logger_default.info(`Saved interview transcript (${transcript.length} Q&A) to issue #${issueNumber}`);
20705
+ }
20706
+ async function updateIdeaFromInsights(issueNumber, insights) {
20707
+ const updates = {};
20708
+ const updatedFields = [];
20709
+ if (insights.hook !== void 0) {
20710
+ updates.hook = insights.hook;
20711
+ updatedFields.push("hook");
20712
+ }
20713
+ if (insights.audience !== void 0) {
20714
+ updates.audience = insights.audience;
20715
+ updatedFields.push("audience");
20716
+ }
20717
+ if (insights.keyTakeaway !== void 0) {
20718
+ updates.keyTakeaway = insights.keyTakeaway;
20719
+ updatedFields.push("keyTakeaway");
20720
+ }
20721
+ if (insights.trendContext !== void 0) {
20722
+ updates.trendContext = insights.trendContext;
20723
+ updatedFields.push("trendContext");
20724
+ }
20725
+ if (insights.talkingPoints !== void 0 && insights.talkingPoints.length > 0) {
20726
+ updates.talkingPoints = insights.talkingPoints;
20727
+ updatedFields.push("talkingPoints");
20728
+ }
20729
+ if (insights.tags !== void 0 && insights.tags.length > 0) {
20730
+ updates.tags = insights.tags;
20731
+ updatedFields.push("tags");
20732
+ }
20733
+ if (updatedFields.length === 0) {
20734
+ logger_default.info(`No fields to update for idea #${issueNumber} from insights`);
20735
+ return;
20736
+ }
20737
+ await updateIdea(issueNumber, updates);
20738
+ logger_default.info(
20739
+ `Updated idea #${issueNumber} fields: ${updatedFields.join(", ")}`
20740
+ );
20741
+ }
20742
+
20743
+ // src/L7-app/commands/ideateStart.ts
20744
+ init_ideaService();
20745
+ var VALID_MODES = ["interview"];
20746
+ async function runIdeateStart(issueNumber, options) {
20747
+ const parsed = Number.parseInt(issueNumber, 10);
20748
+ if (Number.isNaN(parsed) || parsed < 1) {
20749
+ console.error(`Invalid issue number: "${issueNumber}". Must be a positive integer.`);
20750
+ process.exit(1);
20751
+ }
20752
+ initConfig();
20753
+ const mode = options.mode ?? "interview";
20754
+ if (!VALID_MODES.includes(mode)) {
20755
+ console.error(`Unknown mode: "${options.mode}". Valid modes: ${VALID_MODES.join(", ")}`);
20756
+ process.exit(1);
20757
+ }
20758
+ if (options.progress) {
20759
+ interviewEmitter.enable();
20760
+ }
20761
+ const idea = await loadAndValidateIdea(parsed);
20762
+ const chat = new AltScreenChat({
20763
+ title: `\u{1F4DD} Interview: ${idea.topic}`,
20764
+ subtitle: "Type /end to finish the interview. Press Ctrl+C to save and quit.",
20765
+ inputPrompt: "Your answer> "
20766
+ });
20767
+ const answerProvider = async (question, context) => {
20768
+ chat.showQuestion(
20769
+ question,
20770
+ context.rationale,
20771
+ context.targetField ? String(context.targetField) : "general",
20772
+ context.questionNumber
20773
+ );
20774
+ const answer = await chat.promptInput();
20775
+ if (chat.interrupted) {
20776
+ return "/end";
20777
+ }
20778
+ chat.addMessage("agent", question);
20779
+ chat.addMessage("user", answer);
20780
+ return answer;
20781
+ };
20782
+ const handleEvent = (event) => {
20783
+ switch (event.event) {
20784
+ case "thinking:start":
20785
+ chat.setStatus("\u{1F914} Thinking of next question...");
20786
+ break;
20787
+ case "thinking:end":
20788
+ chat.clearStatus();
20789
+ break;
20790
+ case "tool:start":
20791
+ chat.setStatus(`\u{1F527} ${event.toolName}...`);
20792
+ break;
20793
+ case "tool:end":
20794
+ chat.clearStatus();
20795
+ break;
20796
+ case "insight:discovered":
20797
+ chat.showInsight(`${event.field}: ${event.insight}`);
20798
+ break;
20799
+ }
20800
+ };
20801
+ setChatMode(true);
20802
+ chat.enter();
20803
+ chat.addMessage("system", `Starting interview for idea #${idea.issueNumber}: ${idea.topic}`);
20804
+ chat.addMessage("system", "The agent will ask Socratic questions to help develop your idea.");
20805
+ try {
20806
+ const result = await startInterview(idea, answerProvider, handleEvent);
20807
+ await saveResults(result, chat, parsed);
20808
+ } catch (error) {
20809
+ if (error instanceof Error) {
20810
+ chat.addMessage("error", error.message);
20811
+ }
20812
+ throw error;
20813
+ } finally {
20814
+ chat.destroy();
20815
+ setChatMode(false);
20816
+ }
20817
+ }
20818
+ async function saveResults(result, chat, issueNumber) {
20819
+ const durationSec = Math.round(result.durationMs / 1e3);
20820
+ const fieldList = result.updatedFields.length > 0 ? result.updatedFields.join(", ") : "none";
20821
+ chat.showQuestion(
20822
+ `Interview ${result.endedBy === "user" ? "ended" : "completed"} \u2014 ${result.transcript.length} questions in ${durationSec}s`,
20823
+ `Updated fields: ${fieldList}`,
20824
+ "summary",
20825
+ result.transcript.length
20826
+ );
20827
+ if (result.transcript.length > 0) {
20828
+ chat.setStatus("\u{1F4BE} Saving transcript...");
20829
+ await saveTranscript(issueNumber, result.transcript);
20830
+ }
20831
+ if (result.insights && Object.keys(result.insights).length > 0) {
20832
+ chat.setStatus("\u{1F4BE} Updating idea fields...");
20833
+ await updateIdeaFromInsights(issueNumber, result.insights);
20834
+ }
20835
+ chat.clearStatus();
20836
+ chat.showInsight("\u2705 Saved! Mark this idea as ready? (yes/no)");
20837
+ const response = await chat.promptInput();
20838
+ if (response.toLowerCase().startsWith("y")) {
20839
+ await updateIdea(issueNumber, { status: "ready" });
20840
+ chat.showInsight(`\u2705 Idea #${issueNumber} marked as ready`);
20841
+ await new Promise((resolve3) => setTimeout(resolve3, 1500));
20842
+ }
20843
+ }
20844
+
20075
20845
  // src/L1-infra/readline/readlinePromises.ts
20076
- import { createInterface as createInterface2 } from "readline/promises";
20846
+ import { createInterface } from "readline/promises";
20077
20847
  function createPromptInterface(options) {
20078
- return createInterface2({
20848
+ return createInterface({
20079
20849
  input: options?.input ?? process.stdin,
20080
20850
  output: options?.output ?? process.stdout
20081
20851
  });
@@ -21743,6 +22513,10 @@ program.command("chat").description("Interactive chat session with the schedule
21743
22513
  program.command("doctor").description("Check all prerequisites and dependencies").action(async () => {
21744
22514
  await runDoctor();
21745
22515
  });
22516
+ program.command("ideate-start <issue-number>").description("Start an interactive session to develop a content idea").option("--mode <mode>", "Session mode: interview (default)", "interview").option("--progress", "Emit structured JSON interview events to stderr").action(async (issueNumber, opts) => {
22517
+ await runIdeateStart(issueNumber, opts);
22518
+ process.exit(0);
22519
+ });
21746
22520
  program.command("ideate").description("Generate AI-powered content ideas using trend research").option("--topics <topics>", "Comma-separated seed topics").option("--count <n>", "Number of ideas to generate (default: 5)", "5").option("--output <dir>", "Ideas directory (default: ./ideas)").option("--brand <path>", "Brand config path (default: ./brand.json)").option("--list", "List existing ideas instead of generating").option("--status <status>", "Filter by status when listing (draft|ready|recorded|published)").option("--format <format>", "Output format: table (default) or json").option("--add", "Add a single idea (AI-researched by default, or --no-ai for direct)").option("--topic <topic>", "Idea topic/title (required with --add)").option("--hook <hook>", "Attention-grabbing hook (default: topic, --no-ai only)").option("--audience <audience>", "Target audience (default: developers, --no-ai only)").option("--platforms <platforms>", "Comma-separated platforms: tiktok,youtube,instagram,linkedin,x (--no-ai only)").option("--key-takeaway <takeaway>", "Core message the viewer should remember (--no-ai only)").option("--talking-points <points>", "Comma-separated talking points (--no-ai only)").option("--tags <tags>", "Comma-separated categorization tags (--no-ai only)").option("--publish-by <date>", "Publish deadline (ISO 8601 date, default: 14 days from now, --no-ai only)").option("--trend-context <context>", "Why this topic is timely (--no-ai only)").option("--no-ai", "Skip AI research agent \u2014 create directly from CLI flags + defaults").option("-p, --prompt <prompt>", 'Free-form prompt to guide idea generation (e.g., "Cover this article: https://...")').action(async (opts) => {
21747
22521
  initConfig();
21748
22522
  await runIdeate(opts);