scai 0.1.90 → 0.1.92

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/CHANGELOG.md CHANGED
@@ -117,4 +117,8 @@ Type handling with the module pipeline
117
117
  ## 2025-08-18
118
118
 
119
119
  • Add interactive delete repository command
120
- • Refactor DeleteIndex command to handle repository deletion correctly
120
+ • Refactor DeleteIndex command to handle repository deletion correctly
121
+
122
+ ## 2025-08-19
123
+
124
+ * Improved line classification and merging logic
@@ -29,17 +29,19 @@ export async function runInteractiveDelete() {
29
29
  }
30
30
  const selectedKey = keys[index];
31
31
  console.log(`\n⚠️ Deleting repository: ${chalk.red(selectedKey)}\n`);
32
- // Build update: remove repo by setting it to null
32
+ // Build an update that uses the null-sentinel for deletion.
33
33
  const update = { repos: { [selectedKey]: null } };
34
- // Reset activeRepo if it pointed to the deleted one
34
+ // If deleting the active repo, pick another one (first remaining) or unset.
35
35
  if (config.activeRepo === selectedKey) {
36
- const remainingKeys = keys.filter((k) => k !== selectedKey);
36
+ const remainingKeys = keys.filter(k => k !== selectedKey);
37
37
  update.activeRepo = remainingKeys[0] || undefined;
38
38
  console.log(`ℹ️ Active repo reset to: ${chalk.green(update.activeRepo ?? 'none')}`);
39
39
  }
40
40
  try {
41
- writeConfig(update); // only pass the diff
41
+ writeConfig(update); // this will actually remove the repo key
42
42
  console.log(`✅ Repository "${selectedKey}" removed from config.json.`);
43
+ // NOTE: We intentionally do NOT delete the on-disk repo folder here.
44
+ // Only remove data when a --purge flag is explicitly used.
43
45
  }
44
46
  catch (err) {
45
47
  console.error('❌ Failed to update config.json:', err instanceof Error ? err.message : err);
@@ -1,31 +1,28 @@
1
1
  // File: src/commands/switch.ts
2
2
  import readline from 'readline';
3
3
  import { Config, writeConfig } from '../config.js';
4
- import { normalizePath, getRepoKeyForPath } from '../utils/normalizePath.js';
4
+ import { getRepoKeyForPath } from '../utils/normalizePath.js';
5
5
  import chalk from 'chalk';
6
6
  export function runSwitchCommand(inputPathOrKey) {
7
7
  const config = Config.getRaw();
8
- const normalizedInput = normalizePath(inputPathOrKey);
9
- // Try to match by key directly
10
- if (config.repos[normalizedInput]) {
11
- config.activeRepo = normalizedInput;
12
- // Update GitHub token
13
- Config.setGitHubToken(config.repos[normalizedInput].githubToken ?? '');
14
- console.log(`✅ Switched active repo to key: ${normalizedInput}`);
8
+ // Direct key match
9
+ if (config.repos[inputPathOrKey]) {
10
+ config.activeRepo = inputPathOrKey;
11
+ Config.setGitHubToken(config.repos[inputPathOrKey].githubToken ?? '');
12
+ console.log(`✅ Switched active repo to key: ${inputPathOrKey}`);
15
13
  }
16
14
  else {
17
- // Try to match by indexDir path
15
+ // Path match only if key match failed
18
16
  const repoKey = getRepoKeyForPath(inputPathOrKey, config);
19
17
  if (!repoKey) {
20
18
  console.error(`❌ No repo found matching path or key: "${inputPathOrKey}"`);
21
19
  process.exit(1);
22
20
  }
23
21
  config.activeRepo = repoKey;
24
- // Update GitHub token
25
22
  Config.setGitHubToken(config.repos[repoKey]?.githubToken ?? '');
26
23
  console.log(`✅ Switched active repo to path match: ${repoKey}`);
27
24
  }
28
- // Ensure the active repo change is saved back to the config
25
+ // Persist change
29
26
  writeConfig(config);
30
27
  }
31
28
  export async function runInteractiveSwitch() {
package/dist/config.js CHANGED
@@ -2,8 +2,9 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { CONFIG_PATH, SCAI_HOME, SCAI_REPOS } from './constants.js';
4
4
  import { getDbForRepo } from './db/client.js';
5
- import { getRepoKeyForPath, normalizePath } from './utils/normalizePath.js';
5
+ import { normalizePath } from './utils/normalizePath.js';
6
6
  import chalk from 'chalk';
7
+ import { getHashedRepoKey } from './utils/repoKey.js';
7
8
  const defaultConfig = {
8
9
  model: 'llama3',
9
10
  contextLength: 8192,
@@ -30,7 +31,7 @@ export function readConfig() {
30
31
  export function writeConfig(newCfg) {
31
32
  ensureConfigDir();
32
33
  const current = readConfig();
33
- let merged = {
34
+ const merged = {
34
35
  ...current,
35
36
  ...newCfg,
36
37
  repos: {
@@ -38,7 +39,7 @@ export function writeConfig(newCfg) {
38
39
  ...(newCfg.repos || {}),
39
40
  },
40
41
  };
41
- // Special handling: remove repos explicitly set to null
42
+ // Remove repos explicitly set to null
42
43
  if (newCfg.repos) {
43
44
  for (const [key, value] of Object.entries(newCfg.repos)) {
44
45
  if (value === null) {
@@ -86,54 +87,47 @@ export const Config = {
86
87
  }
87
88
  },
88
89
  getIndexDir() {
89
- const config = readConfig();
90
- const activeRepo = config.activeRepo;
91
- if (activeRepo) {
92
- const normalized = normalizePath(activeRepo);
93
- return normalizePath(config.repos[normalized]?.indexDir ?? '');
94
- }
95
- return '';
90
+ const cfg = readConfig();
91
+ const activeRepo = cfg.activeRepo;
92
+ if (!activeRepo)
93
+ return '';
94
+ return cfg.repos[activeRepo]?.indexDir ?? '';
96
95
  },
97
96
  async setIndexDir(indexDir) {
98
- const absPath = path.resolve(indexDir); // Resolve the index directory to an absolute path
99
- const repoKey = normalizePath(absPath); // Normalize path for the repo (get repo name, not full path)
100
- // Ensure repoKey doesn't contain an absolute path, only the repo name or a relative path
101
- const scaiRepoRoot = path.join(SCAI_REPOS, path.basename(repoKey)); // Use repo name as key to avoid double paths
102
- // Set the active repo to the provided indexDir
103
- this.setActiveRepo(scaiRepoRoot);
104
- // Call setRepoIndexDir to update the repo's indexDir and other settings
105
- await this.setRepoIndexDir(scaiRepoRoot, absPath); // Set the indexDir for the repo
97
+ // Normalize the provided index directory
98
+ const normalizedIndexDir = normalizePath(indexDir);
99
+ // Compute a stable repo key
100
+ const repoKey = getHashedRepoKey(normalizedIndexDir);
101
+ const scaiRepoRoot = path.join(SCAI_REPOS, repoKey);
106
102
  // Ensure base folders exist
107
103
  fs.mkdirSync(scaiRepoRoot, { recursive: true });
108
- // Init DB if not exists
104
+ // Set the active repo using the precomputed repoKey
105
+ this.setActiveRepo(repoKey);
106
+ // Update the repo configuration with the normalized indexDir
107
+ await this.setRepoIndexDir(repoKey, normalizedIndexDir);
108
+ // Initialize DB if it does not exist
109
109
  const dbPath = path.join(scaiRepoRoot, 'db.sqlite');
110
110
  if (!fs.existsSync(dbPath)) {
111
111
  console.log(`📦 Database not found. ${chalk.green('Initializing DB')} at ${normalizePath(dbPath)}`);
112
- getDbForRepo(); // Now DB creation works after config update
112
+ getDbForRepo();
113
113
  }
114
114
  },
115
- /**
116
- * Set both the scaiRepoRoot for the config and the indexDir (the actual repo root path)
117
- * @param scaiRepoRoot
118
- * @param indexDir
119
- */
120
- async setRepoIndexDir(scaiRepoRoot, indexDir) {
121
- const normalizedRepoPath = normalizePath(scaiRepoRoot);
122
- const normalizedIndexDir = normalizePath(indexDir);
115
+ async setRepoIndexDir(repoKey, indexDir) {
123
116
  const cfg = readConfig();
124
- if (!cfg.repos[normalizedRepoPath]) {
125
- cfg.repos[normalizedRepoPath] = {};
126
- }
127
- cfg.repos[normalizedRepoPath] = {
128
- ...cfg.repos[normalizedRepoPath],
129
- indexDir: normalizedIndexDir, // Ensure the indexDir is always normalized
117
+ if (!cfg.repos[repoKey])
118
+ cfg.repos[repoKey] = {};
119
+ cfg.repos[repoKey] = {
120
+ ...cfg.repos[repoKey],
121
+ indexDir, // Already normalized
130
122
  };
131
- await writeConfig(cfg); // Persist the config update
132
- console.log(`✅ Repo index directory set for ${normalizedRepoPath} : ${normalizedIndexDir}`);
123
+ await writeConfig(cfg);
124
+ console.log(`✅ Repo index directory set for ${repoKey} : ${indexDir}`);
133
125
  },
134
126
  setActiveRepo(repoKey) {
135
127
  const cfg = readConfig();
136
- cfg.activeRepo = normalizePath(repoKey);
128
+ cfg.activeRepo = repoKey;
129
+ if (!cfg.repos[repoKey])
130
+ cfg.repos[repoKey] = {};
137
131
  writeConfig(cfg);
138
132
  console.log(`✅ Active repo switched to: ${repoKey}`);
139
133
  },
@@ -148,38 +142,27 @@ export const Config = {
148
142
  for (const key of keys) {
149
143
  const r = cfg.repos[key];
150
144
  const isActive = cfg.activeRepo === key;
151
- // Use chalk to ensure proper coloring
152
145
  const label = isActive
153
- ? chalk.green(`✅ ${key} (active)`) // Active repo in green
154
- : chalk.white(` ${key}`); // Inactive repos in white
146
+ ? chalk.green(`✅ ${key} (active)`)
147
+ : chalk.white(` ${key}`);
155
148
  console.log(`- ${label}`);
156
149
  console.log(` ↳ indexDir: ${r.indexDir}`);
157
150
  }
158
151
  },
159
- // Method to get GitHub token for the active repo
160
152
  getGitHubToken() {
161
153
  const cfg = readConfig();
162
154
  const active = cfg.activeRepo;
163
- if (active) {
164
- // Normalize the active repo path and fetch token from repos[activeRepo]
165
- const normalizedActiveRepo = normalizePath(active);
166
- return cfg.repos[normalizedActiveRepo]?.githubToken || null;
167
- }
168
- // If no activeRepo, fall back to the global githubToken field
155
+ if (active)
156
+ return cfg.repos[active]?.githubToken || null;
169
157
  return cfg.githubToken || null;
170
158
  },
171
159
  setGitHubToken(token) {
172
160
  const cfg = readConfig();
173
161
  const active = cfg.activeRepo;
174
162
  if (active) {
175
- const repoKey = getRepoKeyForPath(active, cfg) ?? normalizePath(active);
176
- if (!cfg.repos[repoKey]) {
177
- cfg.repos[repoKey] = {};
178
- }
179
- cfg.repos[repoKey] = {
180
- ...cfg.repos[repoKey],
181
- githubToken: token,
182
- };
163
+ if (!cfg.repos[active])
164
+ cfg.repos[active] = {};
165
+ cfg.repos[active] = { ...cfg.repos[active], githubToken: token };
183
166
  }
184
167
  else {
185
168
  cfg.githubToken = token;
@@ -192,7 +175,7 @@ export const Config = {
192
175
  const active = cfg.activeRepo;
193
176
  console.log(`🔧 Current configuration:`);
194
177
  console.log(` Active index dir: ${active || 'Not Set'}`);
195
- const repoCfg = active ? cfg.repos?.[active] : {};
178
+ const repoCfg = active ? cfg.repos[active] : {};
196
179
  console.log(` Model : ${repoCfg?.model || cfg.model}`);
197
180
  console.log(` Language : ${repoCfg?.language || cfg.language}`);
198
181
  console.log(` GitHub Token : ${cfg.githubToken ? '*****' : 'Not Set'}`);
package/dist/db/client.js CHANGED
@@ -1,31 +1,24 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { SCAI_HOME } from '../constants.js';
4
- import { Config } from '../config.js';
4
+ import { readConfig } from '../config.js';
5
5
  import Database from 'better-sqlite3';
6
6
  /**
7
7
  * Returns a per-repo SQLite database instance.
8
8
  * Ensures the directory and file are created.
9
9
  */
10
- export function getDbForRepo() {
11
- const repoRoot = Config.getIndexDir();
12
- if (!repoRoot) {
13
- throw new Error('No index directory set. Please set an index directory first.');
10
+ export function getDbPathForRepo() {
11
+ const cfg = readConfig();
12
+ const repoKey = cfg.activeRepo;
13
+ if (!repoKey) {
14
+ throw new Error('No active repo set. Please set an index directory first.');
14
15
  }
15
- fs.mkdirSync(SCAI_HOME, { recursive: true });
16
+ return path.join(SCAI_HOME, 'repos', repoKey, 'db.sqlite');
17
+ }
18
+ export function getDbForRepo() {
16
19
  const dbPath = getDbPathForRepo();
17
20
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
18
21
  const db = new Database(dbPath);
19
22
  db.pragma('journal_mode = WAL');
20
23
  return db;
21
24
  }
22
- export function getDbPathForRepo() {
23
- const repoRoot = Config.getIndexDir();
24
- if (!repoRoot) {
25
- throw new Error('No index directory set. Please set an index directory first.');
26
- }
27
- // Use path.basename to get the repo name from the full path
28
- const repoName = path.basename(repoRoot); // Get the last part of the path (the repo name)
29
- const scaiRepoPath = path.join(SCAI_HOME, 'repos', repoName, 'db.sqlite');
30
- return scaiRepoPath;
31
- }
@@ -57,53 +57,48 @@ export const preserveCodeModule = {
57
57
  const fixedLines = [];
58
58
  let origIndex = 0;
59
59
  let newIndex = 0;
60
- while (origIndex < origLines.length && newIndex < newLines.length) {
60
+ // Track all inserted comment blocks globally
61
+ const insertedBlocks = new Set();
62
+ while (origIndex < origLines.length) {
61
63
  const origLine = origLines[origIndex];
62
- const newLine = newLines[newIndex];
63
- // If either current line is a comment treat whole comment block
64
- let lastInsertedModelBlock = [];
65
- if (classifyLine(origLine) !== "code" || classifyLine(newLine) !== "code") {
64
+ // If this is a comment line in original or model
65
+ if (classifyLine(origLine) !== "code" || classifyLine(newLines[newIndex] ?? "") !== "code") {
66
66
  const origBlock = collectBlock(origLines, origIndex);
67
67
  const modelBlock = collectBlock(newLines, newIndex);
68
- // Compare with last inserted block
69
- if (!blocksEqual(modelBlock, lastInsertedModelBlock)) {
70
- if (blocksEqual(origBlock, modelBlock)) {
71
- fixedLines.push(...origBlock);
72
- }
73
- else {
74
- const seen = new Set(trimBlock(modelBlock));
75
- fixedLines.push(...modelBlock);
76
- for (const line of origBlock) {
77
- if (!seen.has(line.trim())) {
78
- fixedLines.push(line);
79
- }
80
- }
68
+ // Merge: model block first, then any orig lines not in model
69
+ const seen = new Set(trimBlock(modelBlock));
70
+ const mergedBlock = [...modelBlock];
71
+ for (const line of origBlock) {
72
+ if (!seen.has(line.trim())) {
73
+ mergedBlock.push(line);
81
74
  }
82
- // Update lastInsertedModelBlock
83
- lastInsertedModelBlock = [...modelBlock];
75
+ }
76
+ // Create a key for duplicate detection
77
+ const mergedKey = JSON.stringify(trimBlock(mergedBlock));
78
+ // Insert only if this block was never inserted before
79
+ if (!insertedBlocks.has(mergedKey)) {
80
+ fixedLines.push(...mergedBlock);
81
+ insertedBlocks.add(mergedKey);
84
82
  }
85
83
  else {
86
84
  console.log("Skipping duplicate block (already inserted)");
87
85
  }
86
+ // Advance indices past the entire blocks
88
87
  origIndex += origBlock.length;
89
88
  newIndex += modelBlock.length;
90
89
  continue;
91
90
  }
92
- // Non-comment lines
93
- if (origLine.trim() === newLine.trim()) {
94
- fixedLines.push(newLine);
95
- }
96
- else {
97
- fixedLines.push(origLine);
98
- }
91
+ // Non-comment line
92
+ const newLine = newLines[newIndex] ?? "";
93
+ fixedLines.push(origLine.trim() === newLine.trim() ? newLine : origLine);
99
94
  origIndex++;
100
95
  newIndex++;
101
96
  }
102
- // Add trailing lines from original (if model ran out first)
97
+ // Add any remaining original lines if model ran out
103
98
  while (origIndex < origLines.length) {
104
99
  fixedLines.push(origLines[origIndex++]);
105
100
  }
106
- // Add trailing comments from model
101
+ // Add trailing comments from model if any
107
102
  while (newIndex < newLines.length) {
108
103
  if (classifyLine(newLines[newIndex]) !== "code") {
109
104
  fixedLines.push(newLines[newIndex]);
@@ -0,0 +1,14 @@
1
+ import crypto from 'crypto';
2
+ import path from 'path';
3
+ import { normalizePath } from './normalizePath.js';
4
+ /**
5
+ * Generate a stable unique key for a repo path.
6
+ * Uses the basename plus a short hash of the full path.
7
+ * Example: "sps-1a2b3c"
8
+ */
9
+ export function getHashedRepoKey(repoPath) {
10
+ const absPath = normalizePath(repoPath); // now cross-platform consistent
11
+ const base = path.basename(absPath);
12
+ const hash = crypto.createHash('md5').update(absPath).digest('hex').slice(0, 6);
13
+ return `${base}-${hash}`;
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.90",
3
+ "version": "0.1.92",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"