ragcode-context-engine 0.1.0

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.
Files changed (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/README.zh-CN.md +363 -0
  4. package/dist/src/cli/configure/app.d.ts +6 -0
  5. package/dist/src/cli/configure/app.js +81 -0
  6. package/dist/src/cli/configure/run.d.ts +5 -0
  7. package/dist/src/cli/configure/run.js +85 -0
  8. package/dist/src/cli/configure/state.d.ts +42 -0
  9. package/dist/src/cli/configure/state.js +174 -0
  10. package/dist/src/cli/configure.d.ts +31 -0
  11. package/dist/src/cli/configure.js +101 -0
  12. package/dist/src/cli/index.d.ts +2 -0
  13. package/dist/src/cli/index.js +503 -0
  14. package/dist/src/cli/tui/index-progress.d.ts +12 -0
  15. package/dist/src/cli/tui/index-progress.js +49 -0
  16. package/dist/src/cli/tui/watch-status.d.ts +10 -0
  17. package/dist/src/cli/tui/watch-status.js +27 -0
  18. package/dist/src/cli/update.d.ts +18 -0
  19. package/dist/src/cli/update.js +111 -0
  20. package/dist/src/config/dotenv.d.ts +1 -0
  21. package/dist/src/config/dotenv.js +14 -0
  22. package/dist/src/config/graph-runtime.d.ts +13 -0
  23. package/dist/src/config/graph-runtime.js +29 -0
  24. package/dist/src/config/runtime-config.d.ts +87 -0
  25. package/dist/src/config/runtime-config.js +215 -0
  26. package/dist/src/config/semantic-runtime.d.ts +24 -0
  27. package/dist/src/config/semantic-runtime.js +89 -0
  28. package/dist/src/context/context-builder.d.ts +20 -0
  29. package/dist/src/context/context-builder.js +277 -0
  30. package/dist/src/context/expansion-policy.d.ts +6 -0
  31. package/dist/src/context/expansion-policy.js +49 -0
  32. package/dist/src/context/skeletonizer.d.ts +2 -0
  33. package/dist/src/context/skeletonizer.js +79 -0
  34. package/dist/src/context/snippet-renderer.d.ts +2 -0
  35. package/dist/src/context/snippet-renderer.js +67 -0
  36. package/dist/src/core/contracts.d.ts +74 -0
  37. package/dist/src/core/contracts.js +1 -0
  38. package/dist/src/core/engine.d.ts +64 -0
  39. package/dist/src/core/engine.js +442 -0
  40. package/dist/src/core/types.d.ts +490 -0
  41. package/dist/src/core/types.js +1 -0
  42. package/dist/src/diagnostics/doctor.d.ts +66 -0
  43. package/dist/src/diagnostics/doctor.js +193 -0
  44. package/dist/src/diagnostics/embedding-test.d.ts +24 -0
  45. package/dist/src/diagnostics/embedding-test.js +83 -0
  46. package/dist/src/graph/diff-files.d.ts +1 -0
  47. package/dist/src/graph/diff-files.js +14 -0
  48. package/dist/src/graph/impact-report.d.ts +10 -0
  49. package/dist/src/graph/impact-report.js +173 -0
  50. package/dist/src/graph/in-memory-graph-store.d.ts +36 -0
  51. package/dist/src/graph/in-memory-graph-store.js +395 -0
  52. package/dist/src/graph/owner-ranking.d.ts +2 -0
  53. package/dist/src/graph/owner-ranking.js +41 -0
  54. package/dist/src/graph/sqlite-graph-store.d.ts +51 -0
  55. package/dist/src/graph/sqlite-graph-store.js +724 -0
  56. package/dist/src/graph/sqlite-statements.d.ts +36 -0
  57. package/dist/src/graph/sqlite-statements.js +105 -0
  58. package/dist/src/graph/target-matcher.d.ts +13 -0
  59. package/dist/src/graph/target-matcher.js +64 -0
  60. package/dist/src/index.d.ts +32 -0
  61. package/dist/src/index.js +32 -0
  62. package/dist/src/indexing/analyzers/fallback-analyzer.d.ts +6 -0
  63. package/dist/src/indexing/analyzers/fallback-analyzer.js +45 -0
  64. package/dist/src/indexing/analyzers/go-treesitter-analyzer.d.ts +2 -0
  65. package/dist/src/indexing/analyzers/go-treesitter-analyzer.js +87 -0
  66. package/dist/src/indexing/analyzers/java-treesitter-analyzer.d.ts +2 -0
  67. package/dist/src/indexing/analyzers/java-treesitter-analyzer.js +88 -0
  68. package/dist/src/indexing/analyzers/python-treesitter-analyzer.d.ts +2 -0
  69. package/dist/src/indexing/analyzers/python-treesitter-analyzer.js +96 -0
  70. package/dist/src/indexing/analyzers/registry.d.ts +5 -0
  71. package/dist/src/indexing/analyzers/registry.js +23 -0
  72. package/dist/src/indexing/analyzers/rust-treesitter-analyzer.d.ts +2 -0
  73. package/dist/src/indexing/analyzers/rust-treesitter-analyzer.js +96 -0
  74. package/dist/src/indexing/analyzers/tree-sitter-base.d.ts +30 -0
  75. package/dist/src/indexing/analyzers/tree-sitter-base.js +163 -0
  76. package/dist/src/indexing/analyzers/types.d.ts +17 -0
  77. package/dist/src/indexing/analyzers/types.js +1 -0
  78. package/dist/src/indexing/analyzers/typescript-analyzer.d.ts +5 -0
  79. package/dist/src/indexing/analyzers/typescript-analyzer.js +199 -0
  80. package/dist/src/indexing/ast-analyzer.d.ts +11 -0
  81. package/dist/src/indexing/ast-analyzer.js +11 -0
  82. package/dist/src/indexing/chunker.d.ts +11 -0
  83. package/dist/src/indexing/chunker.js +157 -0
  84. package/dist/src/indexing/ignore-policy.d.ts +6 -0
  85. package/dist/src/indexing/ignore-policy.js +40 -0
  86. package/dist/src/indexing/indexer.d.ts +13 -0
  87. package/dist/src/indexing/indexer.js +189 -0
  88. package/dist/src/indexing/language.d.ts +3 -0
  89. package/dist/src/indexing/language.js +24 -0
  90. package/dist/src/indexing/scanner.d.ts +13 -0
  91. package/dist/src/indexing/scanner.js +87 -0
  92. package/dist/src/lsp/definition-resolver.d.ts +6 -0
  93. package/dist/src/lsp/definition-resolver.js +60 -0
  94. package/dist/src/lsp/typescript-language-service.d.ts +21 -0
  95. package/dist/src/lsp/typescript-language-service.js +82 -0
  96. package/dist/src/mcp/server.d.ts +11 -0
  97. package/dist/src/mcp/server.js +64 -0
  98. package/dist/src/mcp/tools.d.ts +266 -0
  99. package/dist/src/mcp/tools.js +309 -0
  100. package/dist/src/project/project-identity.d.ts +2 -0
  101. package/dist/src/project/project-identity.js +24 -0
  102. package/dist/src/project/project-registry.d.ts +12 -0
  103. package/dist/src/project/project-registry.js +49 -0
  104. package/dist/src/project/workspace-resolver.d.ts +20 -0
  105. package/dist/src/project/workspace-resolver.js +62 -0
  106. package/dist/src/retrieval/graph-reranker.d.ts +11 -0
  107. package/dist/src/retrieval/graph-reranker.js +0 -0
  108. package/dist/src/retrieval/hybrid-retriever.d.ts +31 -0
  109. package/dist/src/retrieval/hybrid-retriever.js +111 -0
  110. package/dist/src/retrieval/path-classification.d.ts +6 -0
  111. package/dist/src/retrieval/path-classification.js +22 -0
  112. package/dist/src/retrieval/query-matching.d.ts +22 -0
  113. package/dist/src/retrieval/query-matching.js +166 -0
  114. package/dist/src/retrieval/query-planner.d.ts +5 -0
  115. package/dist/src/retrieval/query-planner.js +77 -0
  116. package/dist/src/retrieval/ranking-signals.d.ts +19 -0
  117. package/dist/src/retrieval/ranking-signals.js +97 -0
  118. package/dist/src/retrieval/topology-distance.d.ts +21 -0
  119. package/dist/src/retrieval/topology-distance.js +116 -0
  120. package/dist/src/reuse/reuse-detector.d.ts +12 -0
  121. package/dist/src/reuse/reuse-detector.js +564 -0
  122. package/dist/src/semantic/deterministic-embedding.d.ts +7 -0
  123. package/dist/src/semantic/deterministic-embedding.js +31 -0
  124. package/dist/src/semantic/in-memory-semantic-store.d.ts +11 -0
  125. package/dist/src/semantic/in-memory-semantic-store.js +65 -0
  126. package/dist/src/semantic/lance-semantic-store.d.ts +131 -0
  127. package/dist/src/semantic/lance-semantic-store.js +623 -0
  128. package/dist/src/semantic/openai-compatible-embedding.d.ts +19 -0
  129. package/dist/src/semantic/openai-compatible-embedding.js +75 -0
  130. package/dist/src/service/service-identity.d.ts +13 -0
  131. package/dist/src/service/service-identity.js +48 -0
  132. package/dist/src/service/service-manager.d.ts +29 -0
  133. package/dist/src/service/service-manager.js +231 -0
  134. package/dist/src/service/service-templates.d.ts +22 -0
  135. package/dist/src/service/service-templates.js +101 -0
  136. package/dist/src/subgraph/impact-explainer.d.ts +2 -0
  137. package/dist/src/subgraph/impact-explainer.js +54 -0
  138. package/dist/src/subgraph/node-expander.d.ts +13 -0
  139. package/dist/src/subgraph/node-expander.js +139 -0
  140. package/dist/src/subgraph/output-preset.d.ts +3 -0
  141. package/dist/src/subgraph/output-preset.js +102 -0
  142. package/dist/src/subgraph/subgraph-builder.d.ts +17 -0
  143. package/dist/src/subgraph/subgraph-builder.js +688 -0
  144. package/dist/src/topology/export-index.d.ts +7 -0
  145. package/dist/src/topology/export-index.js +14 -0
  146. package/dist/src/topology/framework-topology.d.ts +3 -0
  147. package/dist/src/topology/framework-topology.js +460 -0
  148. package/dist/src/topology/import-resolver.d.ts +2 -0
  149. package/dist/src/topology/import-resolver.js +29 -0
  150. package/dist/src/topology/orm-topology.d.ts +3 -0
  151. package/dist/src/topology/orm-topology.js +200 -0
  152. package/dist/src/topology/runtime-topology.d.ts +3 -0
  153. package/dist/src/topology/runtime-topology.js +204 -0
  154. package/dist/src/topology/symbol-resolver.d.ts +6 -0
  155. package/dist/src/topology/symbol-resolver.js +74 -0
  156. package/dist/src/topology/test-topology.d.ts +2 -0
  157. package/dist/src/topology/test-topology.js +82 -0
  158. package/dist/src/utils/hash.d.ts +2 -0
  159. package/dist/src/utils/hash.js +7 -0
  160. package/dist/src/utils/path.d.ts +2 -0
  161. package/dist/src/utils/path.js +7 -0
  162. package/dist/src/watch/event-journal.d.ts +17 -0
  163. package/dist/src/watch/event-journal.js +81 -0
  164. package/dist/src/watch/file-event-coalescer.d.ts +9 -0
  165. package/dist/src/watch/file-event-coalescer.js +39 -0
  166. package/dist/src/watch/index-scheduler.d.ts +52 -0
  167. package/dist/src/watch/index-scheduler.js +190 -0
  168. package/dist/src/watch/watch-daemon.d.ts +73 -0
  169. package/dist/src/watch/watch-daemon.js +368 -0
  170. package/dist/src/watch/watcher-liveness.d.ts +47 -0
  171. package/dist/src/watch/watcher-liveness.js +168 -0
  172. package/dist/src/web/server.d.ts +1 -0
  173. package/dist/src/web/server.js +375 -0
  174. package/package.json +94 -0
@@ -0,0 +1,75 @@
1
+ export class OpenAICompatibleEmbeddingProvider {
2
+ options;
3
+ dimensions;
4
+ baseUrl;
5
+ fetchImpl;
6
+ constructor(options) {
7
+ this.options = options;
8
+ if (!options.apiKey)
9
+ throw new Error("OpenAI-compatible embedding provider requires an API key.");
10
+ if (!options.model)
11
+ throw new Error("OpenAI-compatible embedding provider requires a model.");
12
+ this.baseUrl = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "");
13
+ this.dimensions = options.dimensions;
14
+ this.fetchImpl = options.fetch ?? fetch;
15
+ }
16
+ async embed(text) {
17
+ const [embedding] = await this.embedBatch([text]);
18
+ return embedding;
19
+ }
20
+ async embedBatch(texts) {
21
+ if (texts.length === 0)
22
+ return [];
23
+ const payload = await this.requestEmbeddings(texts.length === 1 ? texts[0] : texts);
24
+ const rows = payload.data;
25
+ if (!rows || rows.length !== texts.length) {
26
+ throw new Error(`Embedding response returned ${rows?.length ?? 0} vector(s), expected ${texts.length}.`);
27
+ }
28
+ const embeddings = new Array(texts.length);
29
+ rows.forEach((row, position) => {
30
+ const index = row.index ?? position;
31
+ if (!Number.isInteger(index) || index < 0 || index >= texts.length) {
32
+ throw new Error(`Embedding response included invalid index: ${String(row.index)}.`);
33
+ }
34
+ if (!row.embedding || !row.embedding.every((value) => Number.isFinite(value))) {
35
+ throw new Error("Embedding response did not include a numeric embedding vector.");
36
+ }
37
+ embeddings[index] = row.embedding;
38
+ });
39
+ if (embeddings.some((embedding) => !embedding)) {
40
+ throw new Error("Embedding response did not include all requested vectors.");
41
+ }
42
+ return embeddings;
43
+ }
44
+ async requestEmbeddings(input) {
45
+ const body = {
46
+ model: this.options.model,
47
+ input
48
+ };
49
+ if (this.options.requestDimensions && this.options.dimensions) {
50
+ body.dimensions = this.options.dimensions;
51
+ }
52
+ const response = await this.fetchImpl(`${this.baseUrl}/embeddings`, {
53
+ method: "POST",
54
+ headers: {
55
+ "authorization": `Bearer ${this.options.apiKey}`,
56
+ "content-type": "application/json"
57
+ },
58
+ body: JSON.stringify(body)
59
+ });
60
+ if (!response.ok) {
61
+ let message = `Embedding request failed with HTTP ${response.status}.`;
62
+ try {
63
+ const errorPayload = await response.json();
64
+ if (errorPayload.error?.message)
65
+ message = errorPayload.error.message;
66
+ }
67
+ catch {
68
+ // Non-JSON error body (e.g. a proxy HTML page); keep the HTTP status message.
69
+ }
70
+ // Attach the status so retry classification can see 429/5xx instead of guessing from text.
71
+ throw Object.assign(new Error(message), { status: response.status });
72
+ }
73
+ return await response.json();
74
+ }
75
+ }
@@ -0,0 +1,13 @@
1
+ export type ServicePlatform = "systemd" | "launchd" | "schtasks" | "unsupported";
2
+ export interface ServiceIdentity {
3
+ /** Stable per-repo identifier, e.g. "ragcode-watch-api-3f9c1a2b". */
4
+ serviceName: string;
5
+ /** Absolute, normalized repo root the service watches. */
6
+ repoRoot: string;
7
+ platform: ServicePlatform;
8
+ }
9
+ export declare function detectServicePlatform(platform?: NodeJS.Platform): ServicePlatform;
10
+ export declare function serviceNameForRepo(repoRoot: string): string;
11
+ export declare function launchdLabelForRepo(repoRoot: string): string;
12
+ export declare function scheduledTaskNameForRepo(repoRoot: string): string;
13
+ export declare function resolveServiceIdentity(repoRoot: string, platform?: NodeJS.Platform): ServiceIdentity;
@@ -0,0 +1,48 @@
1
+ import crypto from "node:crypto";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export function detectServicePlatform(platform = os.platform()) {
5
+ switch (platform) {
6
+ case "linux":
7
+ return "systemd";
8
+ case "darwin":
9
+ return "launchd";
10
+ case "win32":
11
+ return "schtasks";
12
+ default:
13
+ return "unsupported";
14
+ }
15
+ }
16
+ function slugifyBasename(repoRoot) {
17
+ const base = path.basename(path.resolve(repoRoot));
18
+ const slug = base
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9]+/g, "-")
21
+ .replace(/^-+|-+$/g, "")
22
+ .slice(0, 32);
23
+ return slug || "repo";
24
+ }
25
+ function repoHash(repoRoot) {
26
+ // Normalize case on platforms with case-insensitive paths so the same repo always hashes the same.
27
+ const normalized = process.platform === "win32" ? path.resolve(repoRoot).toLowerCase() : path.resolve(repoRoot);
28
+ return crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
29
+ }
30
+ export function serviceNameForRepo(repoRoot) {
31
+ return `ragcode-watch-${slugifyBasename(repoRoot)}-${repoHash(repoRoot)}`;
32
+ }
33
+ // launchd labels are conventionally reverse-DNS; Task Scheduler tolerates backslashes as folders.
34
+ // systemd uses the bare name with a .service suffix. Each accessor keeps the platform convention
35
+ // local so the manager doesn't sprinkle platform conditionals around.
36
+ export function launchdLabelForRepo(repoRoot) {
37
+ return `com.ragcode.watch.${repoHash(repoRoot)}`;
38
+ }
39
+ export function scheduledTaskNameForRepo(repoRoot) {
40
+ return `RagCode\\${serviceNameForRepo(repoRoot)}`;
41
+ }
42
+ export function resolveServiceIdentity(repoRoot, platform = os.platform()) {
43
+ return {
44
+ serviceName: serviceNameForRepo(repoRoot),
45
+ repoRoot: path.resolve(repoRoot),
46
+ platform: detectServicePlatform(platform)
47
+ };
48
+ }
@@ -0,0 +1,29 @@
1
+ import { type ServicePlatform } from "./service-identity.js";
2
+ export interface ServiceManagerOptions {
3
+ /** Node/bun executable to launch the watcher with. Defaults to process.execPath. */
4
+ execPath?: string;
5
+ /** Resolved ragcode CLI entry. Defaults to the built dist CLI next to this module. */
6
+ cliEntry?: string;
7
+ /** Extra args appended to `watch <repoRoot>`, e.g. ["--poll"]. */
8
+ extraArgs?: string[];
9
+ home?: string;
10
+ platform?: NodeJS.Platform;
11
+ }
12
+ export interface ServiceActionResult {
13
+ ok: boolean;
14
+ platform: ServicePlatform;
15
+ serviceName: string;
16
+ repoRoot: string;
17
+ message: string;
18
+ /** Path of the unit/plist written, when applicable. */
19
+ unitPath?: string;
20
+ }
21
+ export interface ServiceStatusResult extends ServiceActionResult {
22
+ installed: boolean;
23
+ }
24
+ export declare class UnsupportedPlatformError extends Error {
25
+ constructor(platform: NodeJS.Platform);
26
+ }
27
+ export declare function installWatcherService(repoRoot: string, options?: ServiceManagerOptions): Promise<ServiceActionResult>;
28
+ export declare function uninstallWatcherService(repoRoot: string, options?: ServiceManagerOptions): Promise<ServiceActionResult>;
29
+ export declare function watcherServiceStatus(repoRoot: string, options?: ServiceManagerOptions): Promise<ServiceStatusResult>;
@@ -0,0 +1,231 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { fileURLToPath } from "node:url";
7
+ import { detectServicePlatform, launchdLabelForRepo, resolveServiceIdentity, scheduledTaskNameForRepo, serviceNameForRepo } from "./service-identity.js";
8
+ import { launchdPlistPath, renderLaunchdPlist, renderSystemdUnit, schtasksCreateArgv, schtasksDeleteArgv, schtasksQueryArgv, systemdUserUnitPath } from "./service-templates.js";
9
+ const execFileAsync = promisify(execFile);
10
+ export class UnsupportedPlatformError extends Error {
11
+ constructor(platform) {
12
+ super(`Background watcher services are not supported on this platform (${platform}). Run \`ragcode watch <repo>\` manually or under your own process manager.`);
13
+ this.name = "UnsupportedPlatformError";
14
+ }
15
+ }
16
+ function resolveCliEntry(options) {
17
+ if (options.cliEntry)
18
+ return options.cliEntry;
19
+ // This module lives at dist/src/service/service-manager.js once built; the CLI entry is one
20
+ // directory over at dist/src/cli/index.js. fileURLToPath (not .pathname) is required for correct
21
+ // Windows paths — .pathname yields "/D:/..." which path.resolve mangles.
22
+ return fileURLToPath(new URL("../cli/index.js", import.meta.url));
23
+ }
24
+ function buildSpec(repoRoot, options) {
25
+ return {
26
+ execPath: options.execPath ?? process.execPath,
27
+ cliEntry: resolveCliEntry(options),
28
+ repoRoot: path.resolve(repoRoot),
29
+ serviceName: serviceNameForRepo(repoRoot),
30
+ extraArgs: options.extraArgs
31
+ };
32
+ }
33
+ // `systemctl --user` and `launchctl` exit non-zero for benign "not loaded"/"already loaded" cases;
34
+ // callers decide whether a non-zero exit is fatal. We capture rather than throw so status can report.
35
+ async function run(file, args) {
36
+ try {
37
+ const { stdout, stderr } = await execFileAsync(file, args);
38
+ return { ok: true, stdout, stderr, code: 0 };
39
+ }
40
+ catch (error) {
41
+ const err = error;
42
+ const code = typeof err.code === "number" ? err.code : 1;
43
+ return { ok: false, stdout: err.stdout ?? "", stderr: err.stderr ?? err.message ?? "", code };
44
+ }
45
+ }
46
+ export async function installWatcherService(repoRoot, options = {}) {
47
+ const platform = options.platform ?? os.platform();
48
+ const kind = detectServicePlatform(platform);
49
+ const identity = resolveServiceIdentity(repoRoot, platform);
50
+ const spec = buildSpec(repoRoot, options);
51
+ const home = options.home ?? os.homedir();
52
+ switch (kind) {
53
+ case "systemd": {
54
+ const unitPath = systemdUserUnitPath(identity.serviceName, home);
55
+ await fs.mkdir(path.dirname(unitPath), { recursive: true });
56
+ await fs.writeFile(unitPath, renderSystemdUnit(spec), "utf8");
57
+ await run("systemctl", ["--user", "daemon-reload"]);
58
+ const enable = await run("systemctl", ["--user", "enable", "--now", `${identity.serviceName}.service`]);
59
+ return {
60
+ ok: enable.ok,
61
+ platform: kind,
62
+ serviceName: identity.serviceName,
63
+ repoRoot: identity.repoRoot,
64
+ unitPath,
65
+ message: enable.ok
66
+ ? `Installed and started systemd user service ${identity.serviceName}. It will restart on login. (Enable lingering with \`loginctl enable-linger\` to keep it running while logged out.)`
67
+ : `Wrote unit to ${unitPath} but \`systemctl --user enable --now\` failed: ${enable.stderr.trim()}`
68
+ };
69
+ }
70
+ case "launchd": {
71
+ const label = launchdLabelForRepo(repoRoot);
72
+ const plistPath = launchdPlistPath(label, home);
73
+ await fs.mkdir(path.dirname(plistPath), { recursive: true });
74
+ await fs.writeFile(plistPath, renderLaunchdPlist(spec, label), "utf8");
75
+ // bootout any prior instance so re-install picks up a changed plist, then bootstrap fresh.
76
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
77
+ await run("launchctl", ["bootout", `gui/${uid}/${label}`]);
78
+ const load = await run("launchctl", ["bootstrap", `gui/${uid}`, plistPath]);
79
+ return {
80
+ ok: load.ok,
81
+ platform: kind,
82
+ serviceName: label,
83
+ repoRoot: identity.repoRoot,
84
+ unitPath: plistPath,
85
+ message: load.ok
86
+ ? `Installed and loaded launchd agent ${label}. It will start on login and restart on crash.`
87
+ : `Wrote plist to ${plistPath} but \`launchctl bootstrap\` failed: ${load.stderr.trim()}`
88
+ };
89
+ }
90
+ case "schtasks": {
91
+ const taskName = scheduledTaskNameForRepo(repoRoot);
92
+ const create = await run("schtasks", schtasksCreateArgv(spec, taskName));
93
+ // schtasks creates the task but doesn't run it immediately for onlogon; kick it once now.
94
+ if (create.ok)
95
+ await run("schtasks", ["/run", "/tn", taskName]);
96
+ return {
97
+ ok: create.ok,
98
+ platform: kind,
99
+ serviceName: identity.serviceName,
100
+ repoRoot: identity.repoRoot,
101
+ message: create.ok
102
+ ? `Registered scheduled task ${taskName}. It runs on logon and relaunches every few minutes as a liveness backstop (the per-repo lock makes redundant launches no-ops).`
103
+ : `\`schtasks /create\` failed: ${create.stderr.trim() || create.stdout.trim()}`
104
+ };
105
+ }
106
+ default:
107
+ throw new UnsupportedPlatformError(platform);
108
+ }
109
+ }
110
+ export async function uninstallWatcherService(repoRoot, options = {}) {
111
+ const platform = options.platform ?? os.platform();
112
+ const kind = detectServicePlatform(platform);
113
+ const identity = resolveServiceIdentity(repoRoot, platform);
114
+ const home = options.home ?? os.homedir();
115
+ switch (kind) {
116
+ case "systemd": {
117
+ const unitPath = systemdUserUnitPath(identity.serviceName, home);
118
+ await run("systemctl", ["--user", "disable", "--now", `${identity.serviceName}.service`]);
119
+ await fs.rm(unitPath, { force: true }).catch(() => undefined);
120
+ await run("systemctl", ["--user", "daemon-reload"]);
121
+ return {
122
+ ok: true,
123
+ platform: kind,
124
+ serviceName: identity.serviceName,
125
+ repoRoot: identity.repoRoot,
126
+ unitPath,
127
+ message: `Removed systemd user service ${identity.serviceName}.`
128
+ };
129
+ }
130
+ case "launchd": {
131
+ const label = launchdLabelForRepo(repoRoot);
132
+ const plistPath = launchdPlistPath(label, home);
133
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
134
+ await run("launchctl", ["bootout", `gui/${uid}/${label}`]);
135
+ await fs.rm(plistPath, { force: true }).catch(() => undefined);
136
+ return {
137
+ ok: true,
138
+ platform: kind,
139
+ serviceName: label,
140
+ repoRoot: identity.repoRoot,
141
+ unitPath: plistPath,
142
+ message: `Removed launchd agent ${label}.`
143
+ };
144
+ }
145
+ case "schtasks": {
146
+ const taskName = scheduledTaskNameForRepo(repoRoot);
147
+ const del = await run("schtasks", schtasksDeleteArgv(taskName));
148
+ return {
149
+ ok: del.ok,
150
+ platform: kind,
151
+ serviceName: identity.serviceName,
152
+ repoRoot: identity.repoRoot,
153
+ message: del.ok ? `Removed scheduled task ${taskName}.` : `\`schtasks /delete\` failed (it may not exist): ${del.stderr.trim() || del.stdout.trim()}`
154
+ };
155
+ }
156
+ default:
157
+ throw new UnsupportedPlatformError(platform);
158
+ }
159
+ }
160
+ // Whether the OS registrar knows about the service. This is the registration check (is a unit
161
+ // installed?), distinct from runtime liveness (is the watcher process actually alive and
162
+ // heartbeating?), which readWatcherLiveness answers. doctor/status report both.
163
+ export async function watcherServiceStatus(repoRoot, options = {}) {
164
+ const platform = options.platform ?? os.platform();
165
+ const kind = detectServicePlatform(platform);
166
+ const identity = resolveServiceIdentity(repoRoot, platform);
167
+ const home = options.home ?? os.homedir();
168
+ switch (kind) {
169
+ case "systemd": {
170
+ const unitPath = systemdUserUnitPath(identity.serviceName, home);
171
+ const exists = await fileExists(unitPath);
172
+ const active = exists ? await run("systemctl", ["--user", "is-active", `${identity.serviceName}.service`]) : undefined;
173
+ const state = active ? active.stdout.trim() || active.stderr.trim() : "not-installed";
174
+ return {
175
+ ok: true,
176
+ installed: exists,
177
+ platform: kind,
178
+ serviceName: identity.serviceName,
179
+ repoRoot: identity.repoRoot,
180
+ unitPath,
181
+ message: exists ? `systemd user service ${identity.serviceName}: ${state}` : `No systemd user service installed for ${identity.repoRoot}.`
182
+ };
183
+ }
184
+ case "launchd": {
185
+ const label = launchdLabelForRepo(repoRoot);
186
+ const plistPath = launchdPlistPath(label, home);
187
+ const exists = await fileExists(plistPath);
188
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
189
+ const print = exists ? await run("launchctl", ["print", `gui/${uid}/${label}`]) : undefined;
190
+ return {
191
+ ok: true,
192
+ installed: exists,
193
+ platform: kind,
194
+ serviceName: label,
195
+ repoRoot: identity.repoRoot,
196
+ unitPath: plistPath,
197
+ message: exists ? `launchd agent ${label}: ${print?.ok ? "loaded" : "installed but not loaded"}` : `No launchd agent installed for ${identity.repoRoot}.`
198
+ };
199
+ }
200
+ case "schtasks": {
201
+ const taskName = scheduledTaskNameForRepo(repoRoot);
202
+ const query = await run("schtasks", schtasksQueryArgv(taskName));
203
+ return {
204
+ ok: true,
205
+ installed: query.ok,
206
+ platform: kind,
207
+ serviceName: identity.serviceName,
208
+ repoRoot: identity.repoRoot,
209
+ message: query.ok ? `Scheduled task ${taskName} is registered.` : `No scheduled task registered for ${identity.repoRoot}.`
210
+ };
211
+ }
212
+ default:
213
+ return {
214
+ ok: false,
215
+ installed: false,
216
+ platform: "unsupported",
217
+ serviceName: identity.serviceName,
218
+ repoRoot: identity.repoRoot,
219
+ message: new UnsupportedPlatformError(platform).message
220
+ };
221
+ }
222
+ }
223
+ async function fileExists(filePath) {
224
+ try {
225
+ await fs.access(filePath);
226
+ return true;
227
+ }
228
+ catch {
229
+ return false;
230
+ }
231
+ }
@@ -0,0 +1,22 @@
1
+ export interface ServiceLaunchSpec {
2
+ /** Absolute path to the node/bun executable that runs the CLI. */
3
+ execPath: string;
4
+ /** Absolute path to the ragcode CLI entry (dist/src/cli/index.js) or a resolvable bin name. */
5
+ cliEntry: string;
6
+ repoRoot: string;
7
+ serviceName: string;
8
+ /** Extra args appended after `watch <repoRoot>`, e.g. ["--poll"]. */
9
+ extraArgs?: string[];
10
+ }
11
+ export declare function watchArgv(spec: ServiceLaunchSpec): string[];
12
+ export declare function renderSystemdUnit(spec: ServiceLaunchSpec): string;
13
+ export declare function systemdUserUnitPath(serviceName: string, home?: string): string;
14
+ export declare function renderLaunchdPlist(spec: ServiceLaunchSpec, label: string): string;
15
+ export declare function launchdPlistPath(label: string, home?: string): string;
16
+ export interface SchtasksCreateOptions {
17
+ /** Minutes between liveness relaunch attempts. Defaults to 5. */
18
+ restartIntervalMinutes?: number;
19
+ }
20
+ export declare function schtasksCreateArgv(spec: ServiceLaunchSpec, taskName: string, options?: SchtasksCreateOptions): string[];
21
+ export declare function schtasksDeleteArgv(taskName: string): string[];
22
+ export declare function schtasksQueryArgv(taskName: string): string[];
@@ -0,0 +1,101 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export function watchArgv(spec) {
4
+ return [spec.cliEntry, "watch", spec.repoRoot, "--no-index-on-start", ...(spec.extraArgs ?? [])];
5
+ }
6
+ // ---------------------------------------------------------------------------
7
+ // systemd (Linux) — user unit, no root. Restart=always survives crashes; the OS user session
8
+ // manager (re)starts it on login when enabled. WorkingDirectory pins relative config resolution.
9
+ // ---------------------------------------------------------------------------
10
+ export function renderSystemdUnit(spec) {
11
+ const execStart = [spec.execPath, ...watchArgv(spec)].map(systemdQuote).join(" ");
12
+ return `[Unit]
13
+ Description=RagCode background watcher for ${spec.repoRoot}
14
+ After=default.target
15
+
16
+ [Service]
17
+ Type=simple
18
+ ExecStart=${execStart}
19
+ WorkingDirectory=${spec.repoRoot}
20
+ Restart=always
21
+ RestartSec=5
22
+ # Avoid a crash-loop hammering CPU if the repo is broken; give up after repeated fast failures
23
+ # and let the user re-enable via 'ragcode service install'.
24
+ StartLimitIntervalSec=60
25
+ StartLimitBurst=5
26
+
27
+ [Install]
28
+ WantedBy=default.target
29
+ `;
30
+ }
31
+ // systemd ExecStart tokens are space-split; wrap tokens containing spaces in double quotes.
32
+ function systemdQuote(token) {
33
+ return /\s/.test(token) ? `"${token.replace(/"/g, '\\"')}"` : token;
34
+ }
35
+ export function systemdUserUnitPath(serviceName, home = os.homedir()) {
36
+ return path.join(home, ".config", "systemd", "user", `${serviceName}.service`);
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // launchd (macOS) — LaunchAgent plist. KeepAlive restarts on crash; RunAtLoad starts on login.
40
+ // ---------------------------------------------------------------------------
41
+ export function renderLaunchdPlist(spec, label) {
42
+ const programArgs = [spec.execPath, ...watchArgv(spec)]
43
+ .map((arg) => ` <string>${escapeXml(arg)}</string>`)
44
+ .join("\n");
45
+ return `<?xml version="1.0" encoding="UTF-8"?>
46
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
47
+ <plist version="1.0">
48
+ <dict>
49
+ <key>Label</key>
50
+ <string>${escapeXml(label)}</string>
51
+ <key>ProgramArguments</key>
52
+ <array>
53
+ ${programArgs}
54
+ </array>
55
+ <key>WorkingDirectory</key>
56
+ <string>${escapeXml(spec.repoRoot)}</string>
57
+ <key>RunAtLoad</key>
58
+ <true/>
59
+ <key>KeepAlive</key>
60
+ <true/>
61
+ <key>ThrottleInterval</key>
62
+ <integer>5</integer>
63
+ </dict>
64
+ </plist>
65
+ `;
66
+ }
67
+ function escapeXml(value) {
68
+ return value
69
+ .replace(/&/g, "&amp;")
70
+ .replace(/</g, "&lt;")
71
+ .replace(/>/g, "&gt;");
72
+ }
73
+ export function launchdPlistPath(label, home = os.homedir()) {
74
+ return path.join(home, "Library", "LaunchAgents", `${label}.plist`);
75
+ }
76
+ export function schtasksCreateArgv(spec, taskName, options = {}) {
77
+ // schtasks /tr takes a single string; quote each token so paths with spaces survive.
78
+ const command = [spec.execPath, ...watchArgv(spec)].map(windowsQuote).join(" ");
79
+ const interval = options.restartIntervalMinutes ?? 5;
80
+ return [
81
+ "/create",
82
+ "/tn", taskName,
83
+ "/tr", command,
84
+ "/sc", "onlogon",
85
+ "/rl", "limited",
86
+ // Keep the task alive: repeat every N minutes indefinitely. The lock makes redundant launches no-ops.
87
+ "/ri", String(interval),
88
+ "/du", "9999:59",
89
+ "/f"
90
+ ];
91
+ }
92
+ export function schtasksDeleteArgv(taskName) {
93
+ return ["/delete", "/tn", taskName, "/f"];
94
+ }
95
+ export function schtasksQueryArgv(taskName) {
96
+ return ["/query", "/tn", taskName];
97
+ }
98
+ // schtasks /tr is a single command string; wrap tokens with spaces in double quotes.
99
+ function windowsQuote(token) {
100
+ return /\s/.test(token) ? `"${token}"` : token;
101
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExplainImpactReport, VerifiedCodeSubgraph } from "../core/types.js";
2
+ export declare function buildExplainImpactReport(target: string, subgraph: VerifiedCodeSubgraph): ExplainImpactReport;
@@ -0,0 +1,54 @@
1
+ export function buildExplainImpactReport(target, subgraph) {
2
+ const callerCount = subgraph.nodes.filter((node) => node.role === "caller").length;
3
+ const routeCount = subgraph.nodes.filter((node) => node.role === "route").length;
4
+ const testCount = subgraph.nodes.filter((node) => node.role === "test").length;
5
+ const exportedTargets = subgraph.nodes.filter((node) => node.role === "target" && node.exported).length;
6
+ const unresolvedCount = subgraph.edges.filter((edge) => edge.confidence === "low" || edge.source === "heuristic").length;
7
+ const truncated = subgraph.coverage.some((signal) => signal.name === "budget_truncated" && signal.status === "fail");
8
+ const noPrimaryOwner = subgraph.coverage.some((signal) => signal.name === "primary_owner_found" && signal.status === "fail");
9
+ let riskScore = 0;
10
+ riskScore += callerCount * 2;
11
+ riskScore += routeCount * 2;
12
+ riskScore += exportedTargets * 3;
13
+ riskScore += unresolvedCount * 3;
14
+ if (testCount === 0)
15
+ riskScore += 2;
16
+ if (truncated)
17
+ riskScore += 2;
18
+ if (noPrimaryOwner)
19
+ riskScore += 5;
20
+ const riskReasons = [];
21
+ if (callerCount > 0)
22
+ riskReasons.push(`${callerCount} caller node(s) are in the blast radius.`);
23
+ if (routeCount > 0)
24
+ riskReasons.push(`${routeCount} route/API node(s) are in the blast radius.`);
25
+ if (exportedTargets > 0)
26
+ riskReasons.push(`${exportedTargets} exported target node(s) may be public API surface.`);
27
+ if (testCount === 0)
28
+ riskReasons.push("No explicit tested_by edge was found for the selected target.");
29
+ if (unresolvedCount > 0)
30
+ riskReasons.push(`${unresolvedCount} unresolved or heuristic edge(s) need manual verification.`);
31
+ if (truncated)
32
+ riskReasons.push("The impact subgraph was truncated by budget.");
33
+ if (noPrimaryOwner)
34
+ riskReasons.push("No primary indexed owner matched the impact target.");
35
+ if (riskReasons.length === 0)
36
+ riskReasons.push("Impact is limited to a small verified internal subgraph with test evidence.");
37
+ const riskLevel = riskScore >= 9 ? "high" : riskScore >= 4 ? "medium" : "low";
38
+ const editReadiness = readinessFor(subgraph, riskLevel, noPrimaryOwner, unresolvedCount);
39
+ return {
40
+ target,
41
+ riskLevel,
42
+ riskScore,
43
+ riskReasons,
44
+ editReadiness,
45
+ subgraph
46
+ };
47
+ }
48
+ function readinessFor(subgraph, riskLevel, noPrimaryOwner, unresolvedCount) {
49
+ if (subgraph.coverageSummary.verdict === "not_enough_context" || !subgraph.answerable || noPrimaryOwner)
50
+ return "not_enough_context";
51
+ if (riskLevel === "high" || unresolvedCount > 0)
52
+ return "investigate_only";
53
+ return subgraph.coverageSummary.verdict;
54
+ }
@@ -0,0 +1,13 @@
1
+ import type { CodeChunk, ExpandNodeResult, ExpansionLevel, SymbolNode } from "../core/types.js";
2
+ export interface ExpandNodeBuildInput {
3
+ nodeRef: string;
4
+ chunks: CodeChunk[];
5
+ symbols: SymbolNode[];
6
+ expansionLevel?: ExpansionLevel;
7
+ budgetChars?: number;
8
+ }
9
+ export declare function expandNode(input: ExpandNodeBuildInput): ExpandNodeResult;
10
+ export declare function parseNodeRef(nodeRef: string): {
11
+ filePath: string;
12
+ symbolName?: string;
13
+ };