scai 0.1.127 → 0.1.128

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 CHANGED
@@ -12,14 +12,42 @@ SCAI is your AI coding companion in the terminal. Stay focused on coding while S
12
12
  > ⚠️ **Alpha Version Notice**
13
13
  > If you have previously installed SCAI, please run:
14
14
  >
15
- > ```bash
15
+ > bash
16
16
  > scai db reset && scai index start
17
- > ```
17
+ >
18
18
  >
19
19
  > before using this version.
20
20
 
21
21
  ---
22
22
 
23
+ ## 🧠 Why SCAI?
24
+
25
+ SCAI is not just another AI tool — it's a **developer-first**, **privacy-focused**, and **local-first** coding companion. Here's why SCAI stands out:
26
+
27
+ ### 🔐 **100% Local & Private by Design**
28
+
29
+ Unlike cloud-based AI tools, SCAI runs entirely on your machine. No data leaves your environment — making it ideal for **sensitive codebases** and **GDPR-compliant workflows**.
30
+
31
+ ### 🧠 **Deep Code Understanding**
32
+
33
+ SCAI doesn't just parse code — it **understands** it. With background indexing, static analysis, and language-aware parsing, SCAI helps you explore, refactor, and debug with confidence.
34
+
35
+ ### 📦 **No Token Costs or API Keys**
36
+
37
+ SCAI works offline, with **zero token usage**. You don’t pay for API calls or subscribe to cloud services. Just install and go.
38
+
39
+ ### 🛠️ **Developer-Focused Toolset**
40
+
41
+ From commit message generation to architecture summaries, SCAI integrates directly into your workflow. It's built for developers, by developers.
42
+
43
+ ### 🇩🇰 **Built in Denmark/EU**
44
+
45
+ SCAI is developed in the European Union, ensuring compliance with data protection laws and a focus on privacy-first development.
46
+
47
+ > ✅ SCAI is your **AI coding assistant that respects your privacy**, enhances your productivity, and works **entirely offline**.
48
+
49
+ ---
50
+
23
51
  ## 🗣️ Language Support (Important)
24
52
 
25
53
  SCAI is currently **tested and validated only on the following languages**:
@@ -36,11 +64,11 @@ Other languages may work partially, but analysis quality, indexing accuracy, and
36
64
 
37
65
  ### 1️⃣ Install & Initialize
38
66
 
39
- ```bash
67
+ bash
40
68
  npm install -g scai
41
69
  scai init
42
70
  scai index start
43
- ```
71
+
44
72
 
45
73
  This initializes local models (recommended: `qwen3-coder:30b`) and starts indexing your code repository.
46
74
 
@@ -1,5 +1,6 @@
1
1
  import { builtInModules } from "../pipeline/registry/moduleRegistry.js";
2
2
  import { logInputOutput } from "../utils/promptLogHelper.js";
3
+ import { planResolverStep } from "./planResolverStep.js";
3
4
  import { infoPlanGen } from "./infoPlanGenStep.js";
4
5
  import { understandIntentStep } from "./understandIntentStep.js";
5
6
  import { structuralAnalysisStep } from "./structuralAnalysisStep.js";
@@ -19,17 +20,14 @@ function startTimer() {
19
20
  return () => Date.now() - start;
20
21
  }
21
22
  function logLine(phase, step, ms, desc) {
22
- // Clear current line (removes leftover spinner)
23
23
  process.stdout.write('\r\x1b[K');
24
24
  const suffix = desc ? ` — ${desc}` : "";
25
25
  const timing = typeof ms === "number" ? ` (${ms}ms)` : "";
26
26
  console.log(`[AGENT] ${phase} :: ${step}${suffix}${timing}`);
27
27
  }
28
- /** Helper to display messages to the user from the agent */
29
28
  function userOutput(message) {
30
29
  console.log(`[USER OUTPUT] ${message}`);
31
30
  }
32
- /** Helper to display messages with phase context */
33
31
  function userPhaseOutput(phase, message) {
34
32
  console.log(`[USER OUTPUT] [${phase}] ${message}`);
35
33
  }
@@ -41,7 +39,7 @@ function resolveModuleForAction(action) {
41
39
  /* ───────────────────────── agent ───────────────────────── */
42
40
  export class MainAgent {
43
41
  constructor(context) {
44
- this.spinner = new Spinner(); // spinner for user feedback
42
+ this.spinner = new Spinner();
45
43
  this.runCount = 0;
46
44
  this.maxRuns = 2;
47
45
  this.context = context;
@@ -50,26 +48,22 @@ export class MainAgent {
50
48
  /* ───────────── step executor ───────────── */
51
49
  async executeStep(step, input) {
52
50
  const stop = startTimer();
53
- if (input.context) {
54
- input.context.currentStep = step;
55
- }
51
+ this.context.currentStep = step;
56
52
  const mod = resolveModuleForAction(step.action);
57
53
  if (!mod) {
58
54
  logLine("EXECUTE", step.action, stop(), "skipped (missing module)");
59
55
  return {
60
56
  query: input.query,
61
57
  content: input.content,
62
- data: { skipped: true },
63
- context: input.context
58
+ data: { skipped: true }
64
59
  };
65
60
  }
66
61
  try {
67
- // Update spinner text for user
68
62
  this.spinner.update(`Running step: ${step.action}`);
69
63
  const output = await mod.run({
70
64
  query: step.description ?? input.query,
71
65
  content: input.data ?? input.content,
72
- context: input.context
66
+ context: this.context
73
67
  });
74
68
  if (!output) {
75
69
  throw new Error(`Module "${mod.name}" returned empty output`);
@@ -77,8 +71,7 @@ export class MainAgent {
77
71
  logLine("EXECUTE", step.action, stop());
78
72
  return {
79
73
  query: step.description ?? input.query,
80
- data: output.data,
81
- context: input.context
74
+ data: output.data
82
75
  };
83
76
  }
84
77
  catch (err) {
@@ -92,14 +85,12 @@ export class MainAgent {
92
85
  const stopRun = startTimer();
93
86
  logLine("RUN", `start #${this.runCount}`);
94
87
  logInputOutput("GlobalContext (structured)", "input", this.context);
95
- this.spinner.start(); // start spinner at beginning
96
- /* BOOT */
88
+ this.spinner.start();
89
+ /* ================= BOOT ================= */
97
90
  {
98
91
  const t = startTimer();
99
92
  await understandIntentStep.run({ context: this.context });
100
93
  logLine("BOOT", "understandIntent", t());
101
- // >>> TASK PERSISTENCE ADDITION <<<
102
- // create a task immediately after boot
103
94
  const db = getDbForRepo();
104
95
  const now = new Date().toISOString();
105
96
  const userQuery = this.context.initContext?.userQuery ?? "unknown query";
@@ -109,33 +100,35 @@ export class MainAgent {
109
100
  `).run(userQuery, now, now);
110
101
  this.taskId = result.lastInsertRowid;
111
102
  logLine("TASK", `created task id=${this.taskId}"`);
103
+ logFolderCapsulesSummary(this.context);
112
104
  }
113
- // =====================================================
114
- // INFORMATION ACQUISITION PHASE
115
- // Purpose: gather raw information, no interpretation
116
- // =====================================================
105
+ /* ================= FAST-PATH CHECK ================= */
117
106
  {
118
- const t = startTimer();
119
- await preFileSearchCheckStep(this.context);
120
- logLine("PRECHECK", "preFileSearch", t());
107
+ await planResolverStep.run(this.context);
108
+ const routing = this.context.analysis?.routingDecision;
109
+ if (routing?.decision === "final-answer" && routing.answer) {
110
+ logLine("ROUTING", "fastPathHit", undefined, "returning final answer early");
111
+ return {
112
+ query: this.query,
113
+ data: { finalAnswer: routing.answer, source: "planResolver" }
114
+ };
115
+ }
121
116
  }
117
+ /* ================= INFORMATION ACQUISITION ================= */
122
118
  {
123
- const t = startTimer();
119
+ let t = startTimer();
120
+ await preFileSearchCheckStep(this.context);
121
+ logLine("PRECHECK", "preFileSearch", t());
122
+ t = startTimer();
124
123
  await infoPlanGen.run(this.context);
125
124
  logLine("PLAN", "infoPlanGen", t());
126
125
  }
127
126
  const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
128
- let stepIO = {
129
- query: this.query,
130
- context: this.context
131
- };
127
+ let stepIO = { query: this.query };
132
128
  for (const step of infoPlan.steps.filter(s => s.groups?.includes("info"))) {
133
129
  stepIO = await this.executeStep(step, stepIO);
134
130
  }
135
- // =====================================================
136
- // ANALYSIS PHASE
137
- // Purpose: understand what we have and what is being asked
138
- // =====================================================
131
+ /* ================= ANALYSIS PHASE ================= */
139
132
  {
140
133
  let t = startTimer();
141
134
  await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
@@ -158,10 +151,7 @@ export class MainAgent {
158
151
  this.resetInitContextForLoop();
159
152
  return this.run();
160
153
  }
161
- // =====================================================
162
- // TRANSFORM PHASE
163
- // Purpose: produce concrete changes or artifacts
164
- // =====================================================
154
+ /* ================= TRANSFORM PHASE ================= */
165
155
  {
166
156
  const t = startTimer();
167
157
  await transformPlanGenStep.run(this.context);
@@ -171,10 +161,12 @@ export class MainAgent {
171
161
  for (const step of transformPlan.steps.filter(s => s.groups?.includes("transform"))) {
172
162
  stepIO = await this.executeStep(step, stepIO);
173
163
  }
174
- // =====================================================
175
- // FINALIZE PHASE
176
- // Purpose: commit results and respond to the user
177
- // =====================================================
164
+ /* // ── OPTIONAL DEBUGGING ──
165
+ debugContext(this.context, {
166
+ step: "After transform plan generation",
167
+ note: "Does plan exist?"
168
+ }); */
169
+ /* ================= FINALIZE PHASE ================= */
178
170
  {
179
171
  const t = startTimer();
180
172
  await finalPlanGenStep.run(this.context);
@@ -184,9 +176,12 @@ export class MainAgent {
184
176
  for (const step of finalPlan.steps.filter(s => s.groups?.includes("finalize"))) {
185
177
  stepIO = await this.executeStep(step, stepIO);
186
178
  }
187
- this.spinner.stop(); // stop spinner at the end
179
+ this.spinner.stop();
188
180
  userOutput("All input/output logs can be found at ~/.scai/input_output.log");
189
181
  logLine("RUN", "complete", stopRun());
182
+ // ───────────── LOG FINAL CONTEXT ─────────────
183
+ console.log("\n[DEBUG] Final MainAgent context:");
184
+ console.dir(this.context, { depth: null, colors: true });
190
185
  return stepIO;
191
186
  }
192
187
  /* ───────────── helpers ───────────── */
@@ -196,3 +191,21 @@ export class MainAgent {
196
191
  }
197
192
  }
198
193
  }
194
+ /* ───────────── FOLDER CAPSULES SUMMARY HELPER ───────────── */
195
+ function logFolderCapsulesSummary(context) {
196
+ if (!context.initContext?.folderCapsules?.length)
197
+ return;
198
+ const capsulesSummary = context.initContext.folderCapsules.map(fc => ({
199
+ path: fc.path,
200
+ fileCount: fc.stats?.fileCount ?? 0,
201
+ depth: fc.depth ?? 0,
202
+ confidence: fc.confidence ?? 0,
203
+ roles: fc.roles ?? [],
204
+ concerns: fc.concerns ?? []
205
+ }));
206
+ context.analysis ?? (context.analysis = {});
207
+ context.analysis.folderCapsulesSummary = capsulesSummary;
208
+ const humanReadable = capsulesSummary.map(fc => `- ${fc.path}: ${fc.fileCount} files, depth ${fc.depth}, confidence ${fc.confidence}`).join("\n");
209
+ context.analysis.folderCapsulesHuman = humanReadable;
210
+ logLine("BOOT", "folderCapsulesSummary", undefined, `📂 ${capsulesSummary.length} capsules summarized`);
211
+ }
@@ -2,92 +2,53 @@
2
2
  import { generate } from "../lib/generate.js";
3
3
  import { logInputOutput } from "../utils/promptLogHelper.js";
4
4
  export async function contextReviewStep(context) {
5
- if (!context.plan?.targetFiles || context.plan.targetFiles.length === 0) {
6
- throw new Error("[contextReviewStep] No targetFiles defined in plan.");
7
- }
8
- // ------------------------------
9
- // Authoritative view: only target files
10
- // ------------------------------
11
- const authoritativeFiles = (context.workingFiles ?? [])
12
- .filter(f => context.plan.targetFiles.includes(f.path))
13
- .map((f) => ({
14
- path: f.path,
15
- hasCode: Boolean(f.code),
16
- code: f.code ?? null,
17
- summary: f.summary ?? null,
18
- }));
19
- // ------------------------------
20
- // Structural verification: only target files
21
- // ------------------------------
22
- const structuralProof = context.analysis?.structure
23
- ? {
24
- fileCount: context.analysis.structure.files.filter(f => context.plan.targetFiles.includes(f.path)).length,
25
- files: context.analysis.structure.files
26
- .filter(f => context.plan.targetFiles.includes(f.path))
27
- .map(f => ({
28
- path: f.path,
29
- hasCode: Boolean(authoritativeFiles.find(af => af.path === f.path)?.code),
30
- })),
31
- }
32
- : null;
33
- // ------------------------------
34
- // Include combined architecture analysis
35
- // ------------------------------
36
- const combinedAnalysis = context.analysis?.combinedAnalysis ?? {};
37
- const architectureSummary = combinedAnalysis.architectureSummary ?? "";
38
- const hotspots = combinedAnalysis.hotspots ?? [];
5
+ const analysis = context.analysis;
6
+ if (!analysis)
7
+ throw new Error("[contextReviewStep] No analysis state available.");
8
+ const intent = analysis.intent ?? { intent: "", intentCategory: "", normalizedQuery: "", confidence: 0 };
9
+ const focus = analysis.focus ?? { relevantFiles: [], missingFiles: [], rationale: "" };
10
+ const fileAnalysis = analysis.fileAnalysis ?? {};
11
+ const planDecision = analysis.planSuggestion?.plan ? "planExists" : "needsPlan";
39
12
  // ------------------------------
40
- // Build prompt
13
+ // Build prompt using only conclusions from analysis
41
14
  // ------------------------------
42
15
  const prompt = `
43
- You are a meta-reasoning agent responsible for determining whether the agent
44
- has sufficient information to proceed.
16
+ You are a meta-reasoning agent.
17
+ Your job is to determine whether the agent has sufficient information
18
+ to fulfill the user's intent, based on the analysis that has already been performed.
45
19
 
46
- IMPORTANT CONTRACT:
47
- - Any file listed below with a non-null "code" field MUST be treated as the
48
- complete and authoritative source code for that file.
49
- - Do NOT request file contents again if "code" is present.
20
+ User Intent:
21
+ ${JSON.stringify(intent, null, 2)}
50
22
 
51
- User intent:
52
- ${JSON.stringify(context.analysis?.intent ?? {}, null, 2)}
23
+ Relevant Files:
24
+ ${JSON.stringify(focus.relevantFiles, null, 2)}
25
+ Missing Files:
26
+ ${JSON.stringify(focus.missingFiles, null, 2)}
27
+ Rationale:
28
+ ${focus.rationale ?? "No rationale provided."}
53
29
 
54
- Authoritative source files:
55
- ${JSON.stringify(authoritativeFiles, null, 2)}
30
+ File-level Semantic Analysis:
31
+ ${JSON.stringify(fileAnalysis, null, 2)}
56
32
 
57
- Structural verification:
58
- ${JSON.stringify(structuralProof, null, 2)}
33
+ Plan Suggestion Status: ${planDecision}
59
34
 
60
- Combined architecture analysis:
61
- {
62
- "architectureSummary": ${JSON.stringify(architectureSummary, null, 2)}
63
- }
64
-
65
- Your task:
66
- 1. Determine whether the available information is sufficient to fulfill the user intent.
67
- 2. If NOT sufficient, explicitly list what is missing.
68
- 3. Output STRICT JSON with the following shape:
35
+ Question:
36
+ Based on the above, can the agent proceed with the user's request?
37
+ Output STRICT JSON with shape:
69
38
 
70
39
  {
71
40
  "decision": "loopAgain" | "stop",
72
- "reason": "string",
73
- "missing": string[]
41
+ "reason": "string explaining why or why not",
42
+ "missing": string[] // any additional analysis or files needed
74
43
  }
75
-
76
- Rules:
77
- - If the intent involves modifying, commenting, refactoring, or analyzing code,
78
- and at least one relevant file includes full source code, the context SHOULD
79
- be considered sufficient.
80
- - Do NOT request information that is already present in the authoritative files.
81
44
  `.trim();
82
- // ------------------------------
83
- // Call LLM
84
- // ------------------------------
45
+ logInputOutput("contextReviewStep", "output", prompt);
85
46
  const ai = await generate({
86
47
  query: context.initContext?.userQuery ?? '',
87
48
  content: prompt,
88
49
  });
89
50
  const text = typeof ai.data === "string" ? ai.data : JSON.stringify(ai.data);
90
- logInputOutput("contextReviewHelper", "output", text);
51
+ logInputOutput("contextReviewStep", "output", text);
91
52
  // ------------------------------
92
53
  // Parse JSON or fallback
93
54
  // ------------------------------
@@ -1,9 +1,10 @@
1
1
  import { logInputOutput } from "../utils/promptLogHelper.js";
2
2
  export const planTargetFilesStep = {
3
3
  name: "planTargetFilesStep",
4
- description: "Sync files from analysis.focus into plan.targetFiles. Ensures only workingFiles are moved and does not redo intent or risks.",
4
+ description: "Sync relevant files from analysis.focus into plan.targetFiles, skipping irrelevant files. Ensures only workingFiles are moved and does not redo intent or risks.",
5
5
  groups: ["analysis"],
6
6
  run: async (input) => {
7
+ var _a;
7
8
  const context = input.context;
8
9
  const query = input.query ?? "";
9
10
  if (!context) {
@@ -14,8 +15,9 @@ export const planTargetFilesStep = {
14
15
  logInputOutput("planTargetFilesStep", "output", output.data);
15
16
  return output;
16
17
  }
17
- // Ensure analysis.focus exists
18
- const focusFiles = context.analysis?.focus?.relevantFiles ?? [];
18
+ const analysis = context.analysis;
19
+ const focusFiles = analysis?.focus?.relevantFiles ?? [];
20
+ const fileAnalysisMap = analysis?.fileAnalysis ?? {};
19
21
  if (!focusFiles.length) {
20
22
  const output = {
21
23
  query,
@@ -26,12 +28,23 @@ export const planTargetFilesStep = {
26
28
  }
27
29
  // Ensure plan exists
28
30
  context.plan || (context.plan = {});
29
- // Move relevant files to plan.targetFiles, only if they exist in workingFiles
30
31
  const workingFilePaths = new Set(context.workingFiles?.map(f => f.path) ?? []);
31
32
  const targetFiles = new Set(context.plan.targetFiles ?? []);
32
- focusFiles.forEach(f => {
33
- if (workingFilePaths.has(f)) {
34
- targetFiles.add(f);
33
+ const skippedFiles = [];
34
+ // Ensure discardedFiles array exists
35
+ if (context.analysis?.focus) {
36
+ (_a = context.analysis.focus).discardedFiles || (_a.discardedFiles = []);
37
+ }
38
+ // Add only non-irrelevant files
39
+ focusFiles.forEach(filePath => {
40
+ const analysisEntry = fileAnalysisMap[filePath];
41
+ if (analysisEntry?.intent === "irrelevant") {
42
+ skippedFiles.push(filePath);
43
+ context.analysis.focus.discardedFiles.push(filePath); // add to discardedFiles
44
+ return;
45
+ }
46
+ if (workingFilePaths.has(filePath)) {
47
+ targetFiles.add(filePath);
35
48
  }
36
49
  });
37
50
  context.plan.targetFiles = Array.from(targetFiles);
@@ -39,7 +52,8 @@ export const planTargetFilesStep = {
39
52
  query,
40
53
  data: {
41
54
  movedFiles: Array.from(targetFiles),
42
- notes: "Focus files successfully moved to plan.targetFiles.",
55
+ skippedFiles,
56
+ notes: `Focus files synced to plan.targetFiles. ${skippedFiles.length} irrelevant file(s) skipped and added to discardedFiles.`,
43
57
  },
44
58
  };
45
59
  logInputOutput("planTargetFilesStep", "output", output.data);
@@ -31,22 +31,19 @@ or modifications in the system to achieve the intended task.
31
31
  Intent / task description:
32
32
  ${intentText}
33
33
 
34
- If the intent indicates that this is NOT a coding, refactoring, or inline commenting task,
35
- then return an empty plan object with an empty "steps" array:
36
- { "steps": [] }
37
-
38
34
  Allowed actions (transformation only):
39
35
  ${actionsJson}
40
36
 
41
37
  Task category:
42
38
  ${intentCategory}
43
39
 
44
- Folder structure:
45
- ${context.analysis.folderCapsulesHuman ?? ''}
46
-
47
40
  Existing relevant files:
48
41
  ${JSON.stringify(context.analysis.focus?.relevantFiles ?? {}, null, 2)}
49
42
 
43
+ If the intent indicates that this is NOT a coding, refactoring, or inline commenting task,
44
+ then return an empty plan object with an empty "steps" array:
45
+ { "steps": [] }
46
+
50
47
  Only perform transformations that are safe based on the existing analysis.
51
48
 
52
49
  ⚡ Phase guidance:
@@ -74,6 +71,7 @@ Return a strictly valid JSON plan:
74
71
  let plan = null;
75
72
  // --- Parse strategy ---
76
73
  if (cleaned.data && typeof cleaned.data === 'object') {
74
+ process.stdout.write("\r\x1b[K");
77
75
  console.log('[transformPlanGen][debug] Using parsed JSON from cleanupModule');
78
76
  plan = cleaned.data;
79
77
  }
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ const shellQuote = require('shell-quote');
7
7
  import { createProgram as cmdFactory, withContext } from './commands/factory.js';
8
8
  import { runAskCommand } from './commands/AskCmd.js';
9
9
  import { setRl } from './commands/ReadlineSingleton.js';
10
- const program = cmdFactory(); // single Commander instance
10
+ const program = cmdFactory();
11
11
  let inShell = false;
12
12
  const customCommands = {};
13
13
  // =====================================================
@@ -64,10 +64,15 @@ customCommands.quit = customCommands.exit;
64
64
  export function registerCommand(name, fn) {
65
65
  customCommands[name] = fn;
66
66
  }
67
+ // =====================================================
68
+ // SHELL
69
+ // =====================================================
67
70
  async function startShell() {
68
71
  if (inShell)
69
72
  return;
70
73
  inShell = true;
74
+ // Ensure cursor is visible & blinking
75
+ process.stdout.write('\x1b[?25h');
71
76
  const rl = readline.createInterface({
72
77
  input: process.stdin,
73
78
  output: process.stdout,
@@ -75,70 +80,139 @@ async function startShell() {
75
80
  historySize: 200,
76
81
  });
77
82
  setRl(rl);
83
+ // Explicit """ multiline
84
+ let multilineBuffer = null;
85
+ // Normal buffered input (enter-to-execute)
86
+ let inputBuffer = [];
87
+ // --- Buffered paste flush to fix "double Enter" issue
88
+ let flushHandle = null;
89
+ const flushInputBuffer = () => {
90
+ if (flushHandle)
91
+ return; // already scheduled
92
+ flushHandle = setImmediate(async () => {
93
+ flushHandle = null;
94
+ const fullQuery = inputBuffer.join('\n').trim();
95
+ inputBuffer = [];
96
+ if (fullQuery) {
97
+ await runQuery(fullQuery);
98
+ }
99
+ rl.prompt();
100
+ });
101
+ };
102
+ const showCursor = () => {
103
+ process.stdout.write('\x1b[?25h');
104
+ };
78
105
  rl.prompt();
106
+ showCursor();
79
107
  rl.on('line', async (line) => {
80
108
  try {
81
- const trimmed = line.trim();
82
- if (!trimmed) {
109
+ showCursor();
110
+ // =====================================================
111
+ // Explicit multiline (""" … """)
112
+ // =====================================================
113
+ if (multilineBuffer !== null) {
114
+ if (line.trim() === '"""') {
115
+ const fullQuery = multilineBuffer.join('\n');
116
+ multilineBuffer = null;
117
+ await runQuery(fullQuery);
118
+ rl.prompt();
119
+ showCursor();
120
+ return;
121
+ }
122
+ multilineBuffer.push(line);
123
+ return;
124
+ }
125
+ if (line.trim() === '"""') {
126
+ multilineBuffer = [];
127
+ console.log('(multiline input — end with """)');
128
+ return;
129
+ }
130
+ // =====================================================
131
+ // Empty line = EXECUTE buffered input
132
+ // =====================================================
133
+ if (line.trim() === '') {
134
+ const fullQuery = inputBuffer.join('\n').trim();
135
+ inputBuffer = [];
136
+ if (fullQuery) {
137
+ await runQuery(fullQuery);
138
+ }
83
139
  rl.prompt();
140
+ showCursor();
84
141
  return;
85
142
  }
86
- // --- Shell command
87
- if (trimmed.startsWith('!')) {
88
- const child = spawn(trimmed.slice(1).trim(), { shell: true, stdio: 'inherit' });
89
- child.on('exit', () => rl.prompt());
90
- child.on('error', (err) => { console.error('Shell command error:', err); rl.prompt(); });
143
+ // =====================================================
144
+ // Shell command
145
+ // =====================================================
146
+ if (line.trim().startsWith('!')) {
147
+ const child = spawn(line.trim().slice(1), {
148
+ shell: true,
149
+ stdio: 'inherit',
150
+ });
151
+ child.on('exit', () => {
152
+ rl.prompt();
153
+ showCursor();
154
+ });
91
155
  return;
92
156
  }
93
- // --- Slash commands
94
- if (trimmed.startsWith('/')) {
95
- const argvParts = shellQuote.parse(trimmed.slice(1))
96
- .map((tok) => typeof tok === 'object' ? tok.op ?? tok.pattern ?? '' : String(tok))
157
+ // =====================================================
158
+ // Slash commands
159
+ // =====================================================
160
+ if (line.trim().startsWith('/')) {
161
+ const argvParts = shellQuote
162
+ .parse(line.trim().slice(1))
163
+ .map((tok) => typeof tok === 'object'
164
+ ? tok.op ?? tok.pattern ?? ''
165
+ : String(tok))
97
166
  .filter(Boolean);
98
167
  const cmdName = argvParts[0];
99
168
  if (customCommands[cmdName]) {
100
169
  await customCommands[cmdName]();
101
170
  }
102
171
  else {
103
- try {
104
- await program.parseAsync(argvParts, { from: 'user' });
105
- }
106
- catch (err) {
107
- console.error('Command error:', err instanceof Error ? err.message : err);
108
- }
172
+ await program.parseAsync(argvParts, { from: 'user' });
109
173
  }
110
174
  rl.prompt();
175
+ showCursor();
111
176
  return;
112
177
  }
113
- // --- Bare input → LLM
114
- await withContext(() => runAskCommand(trimmed));
115
- rl.prompt();
178
+ // =====================================================
179
+ // Otherwise accumulate input
180
+ // =====================================================
181
+ inputBuffer.push(line);
182
+ flushInputBuffer(); // schedule auto-flush after paste ends
116
183
  }
117
184
  catch (err) {
118
185
  console.error('REPL error:', err instanceof Error ? err.stack : err);
119
186
  rl.prompt();
187
+ showCursor();
120
188
  }
121
189
  });
122
- rl.on('close', () => { console.log('Bye!'); process.exit(0); });
123
- process.on('SIGINT', () => { console.log('\nExiting REPL...'); rl.close(); });
190
+ rl.on('close', () => {
191
+ showCursor();
192
+ console.log('Bye!');
193
+ process.exit(0);
194
+ });
195
+ process.on('SIGINT', () => {
196
+ console.log('\nExiting REPL...');
197
+ showCursor();
198
+ rl.close();
199
+ });
200
+ process.on('exit', showCursor);
124
201
  }
125
202
  // ---------------- Main -----------------
126
203
  async function main() {
127
204
  process.on('unhandledRejection', (reason) => console.error('Unhandled Rejection:', reason));
128
- process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err.stack ?? err); process.exit(1); });
205
+ process.on('uncaughtException', (err) => {
206
+ console.error('Uncaught Exception:', err.stack ?? err);
207
+ process.exit(1);
208
+ });
129
209
  const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
130
210
  const args = process.argv.slice(2);
131
- // Run REPL if no args OR user typed `scai shell`
132
- if (isInteractive && (args.length === 0 || (args.length === 1 && args[0] === 'shell'))) {
211
+ if (isInteractive &&
212
+ (args.length === 0 || (args.length === 1 && args[0] === 'shell'))) {
133
213
  await startShell();
134
214
  return;
135
215
  }
136
- try {
137
- await program.parseAsync(process.argv);
138
- }
139
- catch (err) {
140
- console.error('CLI Error:', err instanceof Error ? err.message : err);
141
- process.exit(1);
142
- }
216
+ await program.parseAsync(process.argv);
143
217
  }
144
218
  main();
@@ -2,53 +2,36 @@ import { generate } from "../../lib/generate.js";
2
2
  import { cleanupModule } from "./cleanupModule.js";
3
3
  import { logInputOutput } from "../../utils/promptLogHelper.js";
4
4
  import { splitCodeIntoChunks, countTokens } from "../../utils/splitCodeIntoChunk.js";
5
- const SINGLE_SHOT_TOKEN_LIMIT = 1800;
6
- const CHUNK_TOKEN_LIMIT = 1500;
7
- /**
8
- * Remove leading/trailing markdown code fences.
9
- */
5
+ // ───────────── Token limits ─────────────
6
+ const SINGLE_SHOT_TOKEN_LIMIT = 1500; // keep large enough for small files
7
+ const CHUNK_TOKEN_LIMIT = 800; // chunked transformation limit
10
8
  function stripCodeFences(text) {
11
9
  const lines = text.split("\n");
12
- while (lines.length && /^```/.test(lines[0].trim())) {
10
+ while (lines.length && /^```/.test(lines[0].trim()))
13
11
  lines.shift();
14
- }
15
- while (lines.length && /^```/.test(lines[lines.length - 1].trim())) {
12
+ while (lines.length && /^```/.test(lines[lines.length - 1].trim()))
16
13
  lines.pop();
17
- }
18
14
  return lines.join("\n");
19
15
  }
20
- /**
21
- * Heuristic: decide whether chunk output looks unsafe / non-code.
22
- */
23
16
  function isSuspiciousChunkOutput(text, originalChunk) {
24
17
  const trimmed = text.trim();
25
18
  if (!trimmed)
26
19
  return true;
27
- // Explanations or meta text
28
- if (/here is|transformed|updated code|explanation/i.test(trimmed)) {
20
+ if (/here is|transformed|updated code|explanation/i.test(trimmed))
29
21
  return true;
30
- }
31
- // JSON-ish output (we do NOT want JSON in chunk mode)
32
- if (trimmed.startsWith("{") &&
33
- trimmed.endsWith("}")) {
22
+ if (trimmed.startsWith("{") && trimmed.endsWith("}"))
34
23
  return true;
35
- }
36
- // Extremely short compared to original
37
- if (trimmed.length < originalChunk.trim().length * 0.3) {
24
+ if (trimmed.length < originalChunk.trim().length * 0.3)
38
25
  return true;
39
- }
40
26
  return false;
41
27
  }
42
28
  export const codeTransformModule = {
43
29
  name: "codeTransform",
44
- description: "Transforms a single file specified in the current plan step based on user instruction. " +
45
- "Outputs full rewritten file content (materialized state).",
30
+ description: "Transforms a single file specified in the current plan step based on user instruction.",
46
31
  groups: ["transform"],
47
32
  run: async (input) => {
48
- var _a, _b;
49
- const query = typeof input.query === "string"
50
- ? input.query
51
- : String(input.query ?? "");
33
+ var _a, _b, _c, _d;
34
+ const query = typeof input.query === "string" ? input.query : String(input.query ?? "");
52
35
  const context = input.context;
53
36
  if (!context) {
54
37
  return { query, data: { files: [], errors: ["No context provided"] } };
@@ -57,26 +40,23 @@ export const codeTransformModule = {
57
40
  const step = context.currentStep;
58
41
  const targetFile = step?.targetFile;
59
42
  if (!targetFile) {
60
- return {
61
- query,
62
- data: { files: [], errors: ["No targetFile specified in current plan step"] },
63
- };
43
+ return { query, data: { files: [], errors: ["No targetFile specified in current plan step"] } };
64
44
  }
65
45
  const file = workingFiles.find(f => f.path === targetFile);
66
46
  if (!file || typeof file.code !== "string") {
67
- return {
68
- query,
69
- data: { files: [], errors: [`Target file not found or missing code: ${targetFile}`] },
70
- };
47
+ return { query, data: { files: [], errors: [`Target file not found or missing code: ${targetFile}`] } };
71
48
  }
72
49
  const normalizedQuery = context.analysis?.intent?.normalizedQuery ?? query;
73
50
  const outputs = [];
74
51
  const perFileErrors = [];
75
52
  const tokenCount = countTokens(file.code);
76
- // =========================================================================
77
- // 🔹 PATH 1 — SMALL FILE (JSON, strict)
78
- // =========================================================================
53
+ // ───────────── SMALL FILE ─────────────
79
54
  if (tokenCount <= SINGLE_SHOT_TOKEN_LIMIT) {
55
+ logInputOutput("codeTransform", "output", {
56
+ file: file.path,
57
+ tokenCount,
58
+ message: "Starting small file transformation",
59
+ });
80
60
  const prompt = `
81
61
  You are a precise code transformation assistant.
82
62
 
@@ -104,105 +84,106 @@ JSON schema:
104
84
  }
105
85
  `.trim();
106
86
  try {
87
+ logInputOutput("codeTransform", "output", { file: file.path, message: "Sending prompt to LLM" });
107
88
  const llmResponse = await generate({ content: prompt, query });
108
- const cleaned = await cleanupModule.run({
109
- query,
110
- content: llmResponse.data,
89
+ logInputOutput("codeTransform", "output", {
90
+ file: file.path,
91
+ message: "Received LLM response",
92
+ responseSnippet: String(llmResponse.data ?? "").slice(0, 200),
111
93
  });
112
- const structured = typeof cleaned.data === "object"
113
- ? cleaned.data
114
- : JSON.parse(cleaned.data ?? "{}");
115
- const out = Array.isArray(structured.files)
94
+ const cleaned = await cleanupModule.run({ query, content: llmResponse.data });
95
+ const structured = cleaned.data;
96
+ const out = Array.isArray(structured?.files)
116
97
  ? structured.files.find((f) => f.filePath === file.path)
117
98
  : null;
118
- if (!out || typeof out.content !== "string" || !out.content.trim()) {
119
- perFileErrors.push(`Model did not return full content for ${file.path}`);
120
- }
121
- else {
122
- outputs.push({
123
- filePath: file.path,
124
- content: out.content,
125
- notes: out.notes,
126
- });
99
+ if (!out || typeof out.content !== "string") {
100
+ return { query, data: { files: [], errors: [`Model did not return valid content for ${file.path}`] } };
127
101
  }
102
+ const finalContent = out.content.trim() ? out.content : file.code;
103
+ outputs.push({ filePath: file.path, content: finalContent, notes: out.notes });
128
104
  perFileErrors.push(...(structured.errors ?? []));
105
+ context.execution || (context.execution = {});
106
+ (_a = context.execution).codeTransformArtifacts || (_a.codeTransformArtifacts = { files: [] });
107
+ context.execution.codeTransformArtifacts.files = context.execution.codeTransformArtifacts.files.filter(f => f.filePath !== file.path);
108
+ context.execution.codeTransformArtifacts.files.push({
109
+ filePath: file.path,
110
+ content: finalContent,
111
+ notes: perFileErrors.length ? perFileErrors.join("; ") : undefined,
112
+ });
113
+ context.plan || (context.plan = {});
114
+ (_b = context.plan).touchedFiles || (_b.touchedFiles = []);
115
+ if (!context.plan.touchedFiles.includes(file.path))
116
+ context.plan.touchedFiles.push(file.path);
117
+ const output = { query, data: { files: outputs, errors: perFileErrors } };
118
+ logInputOutput("codeTransform", "output", context.execution.codeTransformArtifacts.files);
119
+ return output;
129
120
  }
130
121
  catch (err) {
131
- return {
132
- query,
133
- data: { files: [], errors: [`LLM call or parsing failed: ${err.message}`] },
134
- };
122
+ return { query, data: { files: [], errors: [`LLM call or cleanup failed: ${err.message}`] } };
135
123
  }
136
124
  }
137
- // =========================================================================
138
- // 🔹 PATH 2 — LARGE FILE (chunked, raw text)
139
- // =========================================================================
140
- else {
141
- const chunks = splitCodeIntoChunks(file.code, CHUNK_TOKEN_LIMIT);
142
- const transformedChunks = [];
143
- for (let i = 0; i < chunks.length; i++) {
144
- const chunk = chunks[i];
145
- const prompt = `
125
+ // ───────────── LARGE FILE (chunked) ─────────────
126
+ const chunks = splitCodeIntoChunks(file.code, CHUNK_TOKEN_LIMIT);
127
+ const transformedChunks = [];
128
+ logInputOutput("codeTransform", "output", { file: file.path, chunkCount: chunks.length, message: "Starting chunked transformation" });
129
+ for (let i = 0; i < chunks.length; i++) {
130
+ const chunk = chunks[i];
131
+ // Add user-facing console log here:
132
+ process.stdout.write("\r\x1b[K");
133
+ console.log(` - Processing chunk ${i + 1} of ${chunks.length} for ${file.path}...`);
134
+ logInputOutput("codeTransform", "output", {
135
+ file: file.path,
136
+ chunkIndex: i + 1,
137
+ chunkLength: chunk.length,
138
+ message: "Processing chunk",
139
+ });
140
+ const prompt = `
146
141
  You are a precise code transformation assistant.
147
142
 
148
143
  User instruction (normalized):
149
144
  ${normalizedQuery}
150
145
 
151
- You are given ONE CHUNK of a larger file.
152
-
153
146
  Rules:
154
147
  - Apply the instruction ONLY if relevant to this chunk.
155
148
  - If no change is needed, return the chunk UNCHANGED.
156
- - Do NOT add or remove unrelated code.
157
- - Do NOT reference other chunks.
158
- - Return ONLY code. No JSON. No explanations. No markdown fences.
149
+ - Return ONLY code.
159
150
 
160
151
  FILE: ${file.path}
161
152
  CHUNK ${i + 1} / ${chunks.length}
162
153
  ---
163
154
  ${chunk}
164
155
  `.trim();
165
- try {
166
- logInputOutput("chunks", "output", chunk);
167
- const llmResponse = await generate({ content: prompt, query });
168
- const raw = typeof llmResponse.data === "string"
169
- ? llmResponse.data
170
- : String(llmResponse.data ?? "");
171
- const stripped = stripCodeFences(raw);
172
- if (isSuspiciousChunkOutput(stripped, chunk)) {
173
- transformedChunks.push(chunk);
174
- perFileErrors.push(`Chunk ${i + 1} suspicious output; original preserved.`);
175
- }
176
- else {
177
- transformedChunks.push(stripped);
178
- }
179
- }
180
- catch (err) {
156
+ try {
157
+ const llmResponse = await generate({ content: prompt, query });
158
+ const raw = String(llmResponse.data ?? "");
159
+ const stripped = stripCodeFences(raw);
160
+ if (isSuspiciousChunkOutput(stripped, chunk)) {
181
161
  transformedChunks.push(chunk);
182
- perFileErrors.push(`Chunk ${i + 1} failed; original preserved. Error: ${err.message}`);
162
+ perFileErrors.push(`Chunk ${i + 1} suspicious; original preserved.`);
163
+ logInputOutput("codeTransform", "output", { file: file.path, chunkIndex: i + 1, message: "Suspicious output, original chunk preserved" });
164
+ }
165
+ else {
166
+ transformedChunks.push(stripped);
167
+ logInputOutput("codeTransform", "output", { file: file.path, chunkIndex: i + 1, message: "Chunk transformed successfully" });
183
168
  }
184
169
  }
185
- outputs.push({
186
- filePath: file.path,
187
- content: transformedChunks.join("\n"),
188
- });
170
+ catch (err) {
171
+ transformedChunks.push(chunk);
172
+ perFileErrors.push(`Chunk ${i + 1} failed; original preserved. Error: ${err.message}`);
173
+ logInputOutput("codeTransform", "output", { file: file.path, chunkIndex: i + 1, error: err.message, message: "Chunk transformation failed, original preserved" });
174
+ }
189
175
  }
190
- // =========================================================================
191
- // 🔹 Persist execution artifacts
192
- // =========================================================================
176
+ logInputOutput("codeTransform", "output", { file: file.path, message: "Finished all chunks", totalChunks: chunks.length });
177
+ outputs.push({ filePath: file.path, content: transformedChunks.join("\n") });
193
178
  context.execution || (context.execution = {});
194
- (_a = context.execution).codeTransformArtifacts || (_a.codeTransformArtifacts = { files: [] });
179
+ (_c = context.execution).codeTransformArtifacts || (_c.codeTransformArtifacts = { files: [] });
195
180
  context.execution.codeTransformArtifacts.files.push(...outputs);
196
181
  context.plan || (context.plan = {});
197
- (_b = context.plan).touchedFiles || (_b.touchedFiles = []);
198
- if (!context.plan.touchedFiles.includes(file.path)) {
182
+ (_d = context.plan).touchedFiles || (_d.touchedFiles = []);
183
+ if (!context.plan.touchedFiles.includes(file.path))
199
184
  context.plan.touchedFiles.push(file.path);
200
- }
201
- const output = {
202
- query,
203
- data: { files: outputs, errors: perFileErrors },
204
- };
205
- logInputOutput("codeTransform", "output", output.data);
185
+ const output = { query, data: { files: outputs, errors: perFileErrors } };
186
+ logInputOutput("codeTransform", "output", context.execution.codeTransformArtifacts.files);
206
187
  return output;
207
188
  },
208
189
  };
@@ -1,6 +1,8 @@
1
1
  import { encode } from 'gpt-3-encoder';
2
- export function splitCodeIntoChunks(text, softLimit = 1500, hardLimitMultiplier = 2) {
3
- const hardLimit = softLimit * hardLimitMultiplier;
2
+ export function splitCodeIntoChunks(text, softLimit = 800, // reduced from 1500
3
+ hardLimitMultiplier = 1.5 // reduced from 2, so hard limit ~1200
4
+ ) {
5
+ const hardLimit = Math.floor(softLimit * hardLimitMultiplier);
4
6
  const lines = text.split('\n');
5
7
  const chunks = [];
6
8
  let currentChunkLines = [];
@@ -59,15 +61,13 @@ export function splitCodeIntoChunks(text, softLimit = 1500, hardLimitMultiplier
59
61
  globalBraceDepth = Math.max(0, globalBraceDepth - 1);
60
62
  if (inFunction) {
61
63
  functionBraceDepth = Math.max(0, functionBraceDepth - 1);
62
- if (functionBraceDepth === 0) {
64
+ if (functionBraceDepth === 0)
63
65
  justClosedFunction = true;
64
- }
65
66
  }
66
67
  if (inTryChain) {
67
68
  tryBraceDepth = Math.max(0, tryBraceDepth - 1);
68
- if (tryBraceDepth === 0) {
69
+ if (tryBraceDepth === 0)
69
70
  justClosedTryBlock = true;
70
- }
71
71
  }
72
72
  }
73
73
  else if (char === '(') {
@@ -87,7 +87,6 @@ export function splitCodeIntoChunks(text, softLimit = 1500, hardLimitMultiplier
87
87
  inFunction = false;
88
88
  if (justClosedTryBlock) {
89
89
  inTryBlock = false;
90
- // Only close the chain if we've already seen a handler
91
90
  if (tryChainHasHandler) {
92
91
  inTryChain = false;
93
92
  tryChainHasHandler = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.127",
3
+ "version": "0.1.128",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"