pi-harness-runtime 1.1.391 → 1.1.393

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 (145) hide show
  1. package/AGENTS.md +29 -8
  2. package/harness/telegram-auth.ts +32 -0
  3. package/harness/telegram-update.ts +125 -0
  4. package/index.ts +100 -27
  5. package/package.json +1 -1
  6. package/packages/a2a-adapter/package.json +1 -1
  7. package/packages/agent-policy/package.json +1 -1
  8. package/packages/architecture-generator/package.json +1 -1
  9. package/packages/auth/package.json +1 -1
  10. package/packages/autonomous-refactor/package.json +1 -1
  11. package/packages/autonomous-runtime/package.json +1 -1
  12. package/packages/cache-strategy/package.json +1 -1
  13. package/packages/capability-registry/package.json +1 -1
  14. package/packages/checkpoint/package.json +1 -1
  15. package/packages/cli-plugin-sdk/package.json +1 -1
  16. package/packages/clipboard/package.json +1 -1
  17. package/packages/clipboard-plugin/package.json +1 -1
  18. package/packages/code-generation/package.json +1 -1
  19. package/packages/code-review/package.json +1 -1
  20. package/packages/codex-adapter/package.json +1 -1
  21. package/packages/codex-watcher/package.json +1 -1
  22. package/packages/config-capture/package.json +1 -1
  23. package/packages/connection-runtime/package.json +1 -1
  24. package/packages/context-compiler/package.json +1 -1
  25. package/packages/context-discovery/package.json +1 -1
  26. package/packages/context-manager/package.json +1 -1
  27. package/packages/contracts/dist/workspace-dotenv.d.ts +44 -0
  28. package/packages/contracts/dist/workspace-dotenv.js +111 -0
  29. package/packages/contracts/dist/workspace-env-format.d.ts +129 -6
  30. package/packages/contracts/dist/workspace-env-format.js +283 -11
  31. package/packages/contracts/dist/workspace-env.d.ts +10 -6
  32. package/packages/contracts/dist/workspace-env.js +36 -92
  33. package/packages/contracts/package.json +1 -1
  34. package/packages/contracts/src/workspace-dotenv.ts +114 -0
  35. package/packages/contracts/src/workspace-env-format.ts +318 -13
  36. package/packages/contracts/src/workspace-env.ts +62 -92
  37. package/packages/contracts/test/workspace-env.test.ts +398 -5
  38. package/packages/cookie-sanitizer/package.json +1 -1
  39. package/packages/cost-optimizer/package.json +1 -1
  40. package/packages/dependency-analyzer/package.json +1 -1
  41. package/packages/django-plugin/package.json +1 -1
  42. package/packages/doc-generator/package.json +1 -1
  43. package/packages/durable-review/package.json +1 -1
  44. package/packages/e2e-tester/package.json +1 -1
  45. package/packages/evaluation-engine/package.json +1 -1
  46. package/packages/evaluation-runner/package.json +1 -1
  47. package/packages/event-bus/package.json +1 -1
  48. package/packages/event-sink/package.json +1 -1
  49. package/packages/event-store/package.json +1 -1
  50. package/packages/experience-replay/package.json +1 -1
  51. package/packages/feedback-collector/package.json +1 -1
  52. package/packages/file-copy-helper/package.json +1 -1
  53. package/packages/framework-detector/package.json +1 -1
  54. package/packages/framework-plugin-sdk/package.json +1 -1
  55. package/packages/frappe-plugin/package.json +1 -1
  56. package/packages/generic-web-plugin/package.json +1 -1
  57. package/packages/health-monitor/package.json +1 -1
  58. package/packages/intent-analyzer/package.json +1 -1
  59. package/packages/jev-judge/package.json +1 -1
  60. package/packages/knowledge-graph/package.json +1 -1
  61. package/packages/knowledge-retrieval/package.json +1 -1
  62. package/packages/laravel-plugin/package.json +1 -1
  63. package/packages/learning-engine/package.json +1 -1
  64. package/packages/mcp-adapter/package.json +1 -1
  65. package/packages/memory-engine/package.json +1 -1
  66. package/packages/milestone-manager/package.json +1 -1
  67. package/packages/model-registry/package.json +1 -1
  68. package/packages/nextjs-plugin/package.json +1 -1
  69. package/packages/notification/dist/adapters/telegram-adapter.d.ts +95 -0
  70. package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
  71. package/packages/notification/dist/adapters/telegram-adapter.js +284 -18
  72. package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
  73. package/packages/notification/dist/telegram-auth.d.ts +23 -0
  74. package/packages/notification/dist/telegram-auth.d.ts.map +1 -0
  75. package/packages/notification/dist/telegram-auth.js +32 -0
  76. package/packages/notification/dist/telegram-auth.js.map +1 -0
  77. package/packages/notification/dist/types.d.ts +5 -0
  78. package/packages/notification/dist/types.d.ts.map +1 -1
  79. package/packages/notification/package.json +1 -1
  80. package/packages/notification/src/adapters/telegram-adapter.ts +283 -18
  81. package/packages/notification/src/telegram-auth.ts +34 -0
  82. package/packages/notification/src/types.ts +5 -0
  83. package/packages/observability/package.json +1 -1
  84. package/packages/okf-indexer/package.json +1 -1
  85. package/packages/paperclip-integration/package.json +1 -1
  86. package/packages/performance-optimizer/package.json +1 -1
  87. package/packages/permission-mode/package.json +1 -1
  88. package/packages/privilege-broker/package.json +1 -1
  89. package/packages/project-analyzer/package.json +1 -1
  90. package/packages/project-bootstrap/package.json +1 -1
  91. package/packages/projection-engine/package.json +1 -1
  92. package/packages/prompt-compiler/package.json +1 -1
  93. package/packages/prompt-versioning/package.json +1 -1
  94. package/packages/provider-adapter-sdk/package.json +1 -1
  95. package/packages/provider-router/package.json +1 -1
  96. package/packages/provider-selector/package.json +1 -1
  97. package/packages/providers/package.json +1 -1
  98. package/packages/qdrant-skills/package.json +1 -1
  99. package/packages/quota-manager/package.json +1 -1
  100. package/packages/rate-limiter/package.json +1 -1
  101. package/packages/react-vite-plugin/package.json +1 -1
  102. package/packages/relay/dist/server.js +7 -2
  103. package/packages/relay/dist/types.d.ts +3 -1
  104. package/packages/relay/dist/ws-server.d.ts +2 -9
  105. package/packages/relay/dist/ws-server.js +5 -3
  106. package/packages/relay/package.json +1 -1
  107. package/packages/relay/src/server.ts +7 -2
  108. package/packages/relay/src/types.ts +6 -1
  109. package/packages/relay/src/ws-server.ts +9 -13
  110. package/packages/relay/test/relay-e2e.test.ts +36 -0
  111. package/packages/release-manager/package.json +1 -1
  112. package/packages/requirement-compiler/package.json +1 -1
  113. package/packages/runtime/package.json +1 -1
  114. package/packages/scheduler/package.json +1 -1
  115. package/packages/scheduler-adapter/package.json +1 -1
  116. package/packages/session/package.json +1 -1
  117. package/packages/session-api/package.json +1 -1
  118. package/packages/session-export/package.json +1 -1
  119. package/packages/shared-context/package.json +1 -1
  120. package/packages/skill-mcp-client/package.json +1 -1
  121. package/packages/skill-registry/package.json +1 -1
  122. package/packages/skills/package.json +1 -1
  123. package/packages/sprint-planner/package.json +1 -1
  124. package/packages/subscription-engine/package.json +1 -1
  125. package/packages/task-compiler/package.json +1 -1
  126. package/packages/telegram/package.json +1 -1
  127. package/packages/tencentdb-memory/package.json +1 -1
  128. package/packages/tencentdb-sync/package.json +1 -1
  129. package/packages/test-data-generator/package.json +1 -1
  130. package/packages/test-generator/package.json +1 -1
  131. package/packages/todo-bd-sync/package.json +1 -1
  132. package/packages/token-estimation/package.json +1 -1
  133. package/packages/token-optimizer/package.json +1 -1
  134. package/packages/trajectory/package.json +1 -1
  135. package/packages/tui/package.json +1 -1
  136. package/packages/types/package.json +1 -1
  137. package/packages/voice-api/package.json +1 -1
  138. package/packages/webmcp-tester/package.json +1 -1
  139. package/packages/wiki/package.json +1 -1
  140. package/packages/workflow-events/package.json +1 -1
  141. package/packages/workspace-scanner/package.json +1 -1
  142. package/packages/worktree/package.json +1 -1
  143. package/packages/write-review/package.json +1 -1
  144. package/scripts/unified-webhook-server.ts +141 -14
  145. package/scripts/ws-server.ts +7 -1
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Reading the dotenv files of a workspace.
3
+ *
4
+ * This module owns the file system part of the environment detection: it
5
+ * finds the ".env" files of a workspace, reads them and skips what must not
6
+ * be read (templates, huge files, unreadable files).
7
+ *
8
+ * It is deliberately free of any other dependency (no node:sqlite) so it
9
+ * can be unit tested everywhere: the detection module itself needs
10
+ * node:sqlite, which not every runtime provides.
11
+ */
12
+ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
13
+ import { basename, join, relative, resolve, sep } from "node:path";
14
+ import { orderDotenvFileNames } from "./workspace-env-format.js";
15
+ /**
16
+ * The biggest dotenv file that is read. A ".env" file is a few kilobytes;
17
+ * anything bigger is not a configuration file (and reading it on every
18
+ * session start would be a waste).
19
+ */
20
+ export const DOTENV_MAX_BYTES = 256 * 1024;
21
+ /**
22
+ * True for a template file that only documents the variables
23
+ * (".env.example", ".env.sample", ".env.template", ...).
24
+ *
25
+ * A template is not the configuration of the workspace: it must never
26
+ * decide the mode. The marker is looked for in EVERY segment of the
27
+ * suffix, so ".env.example.local" is a template too.
28
+ */
29
+ export function isTemplateDotenvName(name) {
30
+ if (name === ".env")
31
+ return false;
32
+ if (!name.startsWith(".env."))
33
+ return false;
34
+ const suffix = name.slice(".env.".length);
35
+ return suffix
36
+ .split(".")
37
+ .some((segment) => /^(examples?|samples?|templates?|tpls?|dists?|defaults?)$/i.test(segment));
38
+ }
39
+ /**
40
+ * True when the file stays inside the workspace.
41
+ *
42
+ * A symlink may point anywhere: a ".env" that links to /etc/... is not the
43
+ * configuration of the workspace and must not be read.
44
+ */
45
+ function isInsideWorkspace(workspacePath, filePath) {
46
+ try {
47
+ const root = realpathSync(resolve(workspacePath));
48
+ const target = realpathSync(resolve(filePath));
49
+ const rel = relative(root, target);
50
+ if (rel === "")
51
+ return true;
52
+ return !rel.startsWith("..") && !rel.startsWith(sep);
53
+ }
54
+ catch {
55
+ // An unreadable path cannot be inside the workspace.
56
+ return false;
57
+ }
58
+ }
59
+ /**
60
+ * The dotenv files of a workspace as FULL paths, in the order they are read.
61
+ *
62
+ * ".env" comes first, then the well known variants, then every other
63
+ * ".env.*" file. Template files are skipped.
64
+ */
65
+ export function listDotenvFiles(workspacePath) {
66
+ let entries = [];
67
+ try {
68
+ entries = readdirSync(workspacePath);
69
+ }
70
+ catch {
71
+ entries = [];
72
+ }
73
+ const names = entries.filter((name) => (name === ".env" || name.startsWith(".env.")) && !isTemplateDotenvName(name));
74
+ return orderDotenvFileNames(names)
75
+ .filter((name) => {
76
+ try {
77
+ const path = join(workspacePath, name);
78
+ // isFile() is false for a directory named ".env".
79
+ if (!statSync(path).isFile())
80
+ return false;
81
+ // A symlink out of the workspace is not a workspace file.
82
+ return isInsideWorkspace(workspacePath, path);
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ })
88
+ .map((name) => join(workspacePath, name));
89
+ }
90
+ /**
91
+ * Read the dotenv files of a workspace.
92
+ *
93
+ * The files are read through the full paths of listDotenvFiles, so the
94
+ * workspace - not the current working directory - decides which files are
95
+ * read.
96
+ */
97
+ export function readDotenvFiles(workspacePath) {
98
+ const files = [];
99
+ for (const path of listDotenvFiles(workspacePath)) {
100
+ try {
101
+ const stats = statSync(path);
102
+ if (stats.size > DOTENV_MAX_BYTES)
103
+ continue;
104
+ files.push({ name: basename(path), content: readFileSync(path, "utf8") });
105
+ }
106
+ catch {
107
+ // Ignore unreadable files.
108
+ }
109
+ }
110
+ return files;
111
+ }
@@ -5,9 +5,12 @@
5
5
  * node:sqlite dependency (bun 1.3.x has no node:sqlite module).
6
6
  *
7
7
  * Wording rules:
8
- * - The mode is only reported when it is EXPLICIT (NODE_ENV). An unknown mode
9
- * is always shown with its reason, never as a bare word next to a dev
10
- * server state that could be read as proof of the mode.
8
+ * - The mode is only reported when it is EXPLICIT: the NODE_ENV of the
9
+ * process env, or a variable of the dotenv files of the workspace -
10
+ * NODE_ENV, an environment shaped key whose value names a known mode
11
+ * (e.g. AMOS_ENV=development) or any other environment shaped key. An
12
+ * unknown mode is always shown with its reason, never as a bare word next
13
+ * to a dev server state that could be read as proof of the mode.
11
14
  * - A dev/build script is a CAPABILITY, not a state. The status line says
12
15
  * "not running (script available)" so it cannot be mistaken for a running
13
16
  * server or for evidence about the workspace mode.
@@ -24,6 +27,17 @@ export interface EnvEvidence {
24
27
  key: string;
25
28
  value: string;
26
29
  timestamp: number;
30
+ /** The dotenv file a value came from (".env", ".env.local", ...). */
31
+ file?: string;
32
+ }
33
+ /**
34
+ * A variable found in a dotenv file.
35
+ */
36
+ export interface DotenvVar {
37
+ key: string;
38
+ value: string;
39
+ /** The file the variable came from ("" for the process env). */
40
+ file?: string;
27
41
  }
28
42
  /**
29
43
  * Workspace environment result
@@ -95,13 +109,122 @@ export declare function filterOwnPids(pids: string[], ownPids: string[]): string
95
109
  * "/home/frappe/app", but not of "/home/frappeX".
96
110
  */
97
111
  export declare function formatWorkspacePath(workspacePath: string, home?: string): string;
112
+ /**
113
+ * Map a raw value to a workspace mode.
114
+ *
115
+ * Returns null for a value that names no known mode ("staging", ""): the
116
+ * caller then reports the mode as unknown and names the value.
117
+ */
118
+ export declare function modeFromValue(value: string): WorkspaceMode | null;
119
+ /**
120
+ * Parse a dotenv file into its variables.
121
+ *
122
+ * Only the name and the value are kept. A dotenv file holds secrets, so the
123
+ * detector must not copy more than it needs.
124
+ */
125
+ export declare function parseDotenvVars(content: string): DotenvVar[];
126
+ /**
127
+ * The placeholder for a value that must not be shown.
128
+ */
129
+ export declare const MASKED_MODE_VALUE = "<set>";
130
+ /**
131
+ * True for a key that names the environment.
132
+ *
133
+ * "NODE_ENV", "AMOS_ENV", "RAILS_ENV", "APP_ENVIRONMENT", "APP_MODE",
134
+ * "APP_STAGE" - but not "BRANCH" or "DEBUG": a key must look like it holds
135
+ * the name of an environment before its value may decide the mode.
136
+ */
137
+ export declare function isEnvShapedKey(key: string): boolean;
138
+ /**
139
+ * True for a key that may hold a secret.
140
+ */
141
+ export declare function isSecretLikeKey(key: string): boolean;
142
+ /**
143
+ * True when the value of the variable may be shown.
144
+ *
145
+ * The value ends up in the agent context and in the SQLite cache, so it is
146
+ * only shown when it is provably not a secret: the NODE_ENV of the process
147
+ * env, or a value that IS the name of the mode.
148
+ */
149
+ export declare function canShowModeValue(key: string, value: string, source: "process.env" | ".env file"): boolean;
150
+ /**
151
+ * Find the variable that names the environment.
152
+ *
153
+ * The search order is the priority the user described:
154
+ * 1. NODE_ENV
155
+ * 2. an environment shaped key whose value names a known mode
156
+ * (development, production, test) - e.g. AMOS_ENV=development
157
+ * 3. any other environment shaped key (e.g. AMOS_ENV, RAILS_ENV) - it may
158
+ * be empty or name a mode this detector does not know
159
+ *
160
+ * A usable key wins over an unusable one: an earlier "APP_MODE=staging" or
161
+ * an empty "APP_ENV=" of the same file must not shadow the
162
+ * "AMOS_ENV=development" of that file. The value match is switched off for
163
+ * the process env (an unrelated variable that happens to hold the word
164
+ * "development" says nothing about the environment) and it is restricted to
165
+ * environment shaped keys: "BRANCH=production" does not make a production
166
+ * workspace.
167
+ *
168
+ * The value match is OPT-IN: a caller has to ask for it
169
+ * (`allowValueMatch: true`).
170
+ */
171
+ export declare function detectModeHint(vars: DotenvVar[], options?: {
172
+ allowValueMatch?: boolean;
173
+ }): DotenvVar | null;
174
+ /**
175
+ * Order the dotenv file names of a workspace.
176
+ *
177
+ * The base ".env" wins over the variants, the well known variants come in a
178
+ * fixed order and every other ".env.*" file follows alphabetically.
179
+ */
180
+ export declare function orderDotenvFileNames(names: string[]): string[];
181
+ /**
182
+ * Read the variables of the dotenv files of a workspace.
183
+ *
184
+ * The first file that defines a key wins (see orderDotenvFileNames).
185
+ */
186
+ export declare function collectDotenvVars(files: {
187
+ name: string;
188
+ content: string;
189
+ }[]): DotenvVar[];
190
+ /**
191
+ * The variables of the process env that can name the environment.
192
+ *
193
+ * Only NODE_ENV and the environment shaped keys are collected: an unrelated
194
+ * variable of the process env must not outrank the workspace files.
195
+ */
196
+ export declare function collectProcessEnvVars(env?: NodeJS.ProcessEnv): DotenvVar[];
197
+ /**
198
+ * The result of the mode detection: the mode plus the variable it came from.
199
+ */
200
+ export interface ModeResolution {
201
+ mode: WorkspaceMode;
202
+ hint: DotenvVar | null;
203
+ source: "process.env" | ".env file" | null;
204
+ }
205
+ /**
206
+ * Resolve the workspace mode from the variables that were found.
207
+ *
208
+ * The order is:
209
+ * 1. the NODE_ENV of the process env - it decides, also when it names no
210
+ * known mode (then the mode stays unknown and the label names it: an
211
+ * explicit "staging" must not be overwritten by a file)
212
+ * 2. the dotenv files of the workspace: NODE_ENV, then an environment
213
+ * shaped key WHOSE VALUE names a known mode (e.g. AMOS_ENV=development),
214
+ * then any other environment shaped key (so that the label can name it)
215
+ * 3. an environment shaped key of the process env (the files are silent)
216
+ */
217
+ export declare function resolveWorkspaceMode(processVars: DotenvVar[], dotenvVars: DotenvVar[]): ModeResolution;
98
218
  /**
99
219
  * Human readable mode label.
100
220
  *
101
221
  * "unknown" always carries the reason: the mode can only be proven by an
102
- * explicit NODE_ENV (process env or .env file). When a NODE_ENV IS present
103
- * but names a mode this detector does not know ("staging"), the label says
104
- * exactly that instead of claiming there is no NODE_ENV at all.
222
+ * explicit variable - NODE_ENV, another *_ENV key (e.g. AMOS_ENV) or a
223
+ * value that names a known mode. When such a variable IS present but names
224
+ * a mode this detector does not know ("staging"), the label says exactly
225
+ * that instead of claiming there is no variable at all.
226
+ *
227
+ * A known mode names its source: "DEVELOPMENT (from AMOS_ENV in .env)".
105
228
  */
106
229
  export declare function describeWorkspaceMode(env: WorkspaceEnvResult): string;
107
230
  /**
@@ -5,9 +5,12 @@
5
5
  * node:sqlite dependency (bun 1.3.x has no node:sqlite module).
6
6
  *
7
7
  * Wording rules:
8
- * - The mode is only reported when it is EXPLICIT (NODE_ENV). An unknown mode
9
- * is always shown with its reason, never as a bare word next to a dev
10
- * server state that could be read as proof of the mode.
8
+ * - The mode is only reported when it is EXPLICIT: the NODE_ENV of the
9
+ * process env, or a variable of the dotenv files of the workspace -
10
+ * NODE_ENV, an environment shaped key whose value names a known mode
11
+ * (e.g. AMOS_ENV=development) or any other environment shaped key. An
12
+ * unknown mode is always shown with its reason, never as a bare word next
13
+ * to a dev server state that could be read as proof of the mode.
11
14
  * - A dev/build script is a CAPABILITY, not a state. The status line says
12
15
  * "not running (script available)" so it cannot be mistaken for a running
13
16
  * server or for evidence about the workspace mode.
@@ -94,23 +97,292 @@ export function formatWorkspacePath(workspacePath, home) {
94
97
  }
95
98
  return path;
96
99
  }
100
+ /**
101
+ * Map a raw value to a workspace mode.
102
+ *
103
+ * Returns null for a value that names no known mode ("staging", ""): the
104
+ * caller then reports the mode as unknown and names the value.
105
+ */
106
+ export function modeFromValue(value) {
107
+ const v = value.trim().toLowerCase();
108
+ if (v === "production" || v === "prod")
109
+ return "production";
110
+ if (v === "development" || v === "dev")
111
+ return "development";
112
+ if (v === "test" || v === "testing")
113
+ return "test";
114
+ return null;
115
+ }
116
+ /**
117
+ * Read the value of one dotenv line.
118
+ *
119
+ * Handles: KEY=value, KEY: value, export KEY=value, quoted values and a
120
+ * trailing comment. A quoted value keeps everything up to the closing quote
121
+ * (a "#" inside the quotes is part of the value). An inline comment of an
122
+ * unquoted value needs the space (" #"), like npm dotenv: a "#" inside a
123
+ * value ("abc#123") stays part of the value.
124
+ */
125
+ function parseDotenvValue(raw) {
126
+ const value = raw.trim();
127
+ if (value.startsWith('"') || value.startsWith("'")) {
128
+ const quote = value[0];
129
+ const end = value.indexOf(quote, 1);
130
+ return end === -1 ? value.slice(1) : value.slice(1, end);
131
+ }
132
+ const hash = value.indexOf(" #");
133
+ if (hash !== -1)
134
+ return value.slice(0, hash).trim();
135
+ return value;
136
+ }
137
+ /**
138
+ * Parse a dotenv file into its variables.
139
+ *
140
+ * Only the name and the value are kept. A dotenv file holds secrets, so the
141
+ * detector must not copy more than it needs.
142
+ */
143
+ export function parseDotenvVars(content) {
144
+ const vars = [];
145
+ for (const rawLine of content.split("\n")) {
146
+ const line = rawLine.trim();
147
+ if (!line || line.startsWith("#"))
148
+ continue;
149
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*[=:]\s*(.*)$/);
150
+ if (!match)
151
+ continue;
152
+ vars.push({ key: match[1], value: parseDotenvValue(match[2]) });
153
+ }
154
+ return vars;
155
+ }
156
+ /**
157
+ * The placeholder for a value that must not be shown.
158
+ */
159
+ export const MASKED_MODE_VALUE = "<set>";
160
+ /**
161
+ * True for a key that names the environment.
162
+ *
163
+ * "NODE_ENV", "AMOS_ENV", "RAILS_ENV", "APP_ENVIRONMENT", "APP_MODE",
164
+ * "APP_STAGE" - but not "BRANCH" or "DEBUG": a key must look like it holds
165
+ * the name of an environment before its value may decide the mode.
166
+ */
167
+ export function isEnvShapedKey(key) {
168
+ return /(^|_)(ENV|ENVIRONMENT|MODE|STAGE)$/i.test(key);
169
+ }
170
+ /**
171
+ * True for a key that may hold a secret.
172
+ */
173
+ export function isSecretLikeKey(key) {
174
+ return /(SECRET|TOKEN|PASSWORD|PASSWD|PWD|KEY|CREDENTIAL|AUTH)/i.test(key);
175
+ }
176
+ /**
177
+ * True when the value of the variable may be shown.
178
+ *
179
+ * The value ends up in the agent context and in the SQLite cache, so it is
180
+ * only shown when it is provably not a secret: the NODE_ENV of the process
181
+ * env, or a value that IS the name of the mode.
182
+ */
183
+ export function canShowModeValue(key, value, source) {
184
+ if (isSecretLikeKey(key))
185
+ return false;
186
+ if (source === "process.env" && key === "NODE_ENV")
187
+ return true;
188
+ return modeFromValue(value) !== null;
189
+ }
190
+ /**
191
+ * Find the variable that names the environment.
192
+ *
193
+ * The search order is the priority the user described:
194
+ * 1. NODE_ENV
195
+ * 2. an environment shaped key whose value names a known mode
196
+ * (development, production, test) - e.g. AMOS_ENV=development
197
+ * 3. any other environment shaped key (e.g. AMOS_ENV, RAILS_ENV) - it may
198
+ * be empty or name a mode this detector does not know
199
+ *
200
+ * A usable key wins over an unusable one: an earlier "APP_MODE=staging" or
201
+ * an empty "APP_ENV=" of the same file must not shadow the
202
+ * "AMOS_ENV=development" of that file. The value match is switched off for
203
+ * the process env (an unrelated variable that happens to hold the word
204
+ * "development" says nothing about the environment) and it is restricted to
205
+ * environment shaped keys: "BRANCH=production" does not make a production
206
+ * workspace.
207
+ *
208
+ * The value match is OPT-IN: a caller has to ask for it
209
+ * (`allowValueMatch: true`).
210
+ */
211
+ export function detectModeHint(vars, options = {}) {
212
+ const nodeEnv = vars.find((v) => v.key === "NODE_ENV");
213
+ if (nodeEnv)
214
+ return nodeEnv;
215
+ // The value match is opt-in: a caller that passes the process env
216
+ // variables must not let an unrelated variable that happens to hold the
217
+ // word "development" decide the mode.
218
+ if (options.allowValueMatch === true) {
219
+ // An environment shaped key whose value names a known mode wins: an
220
+ // earlier "APP_MODE=staging" or an empty "APP_ENV=" of the same file
221
+ // must not shadow the "AMOS_ENV=development" of the same file.
222
+ const usable = vars.find((v) => v.key !== "NODE_ENV" &&
223
+ isEnvShapedKey(v.key) &&
224
+ modeFromValue(v.value) !== null);
225
+ if (usable)
226
+ return usable;
227
+ }
228
+ // No usable key: fall back to the first environment shaped key so that
229
+ // the label can name it (it names no known mode).
230
+ return vars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key)) ?? null;
231
+ }
232
+ /**
233
+ * The dotenv files of a workspace, in the order they are read.
234
+ *
235
+ * ".env" comes first, then the well known variants, then every other
236
+ * ".env.*" file. The first file that defines A KEY wins: the base ".env" is
237
+ * the checked in default, so a key that is only defined in a variant is
238
+ * read from the variant (the precedence is per key, not per mode).
239
+ */
240
+ const DOTENV_FILE_ORDER = [
241
+ ".env",
242
+ ".env.local",
243
+ ".env.development",
244
+ ".env.dev",
245
+ ".env.production",
246
+ ".env.test",
247
+ ];
248
+ /**
249
+ * Order the dotenv file names of a workspace.
250
+ *
251
+ * The base ".env" wins over the variants, the well known variants come in a
252
+ * fixed order and every other ".env.*" file follows alphabetically.
253
+ */
254
+ export function orderDotenvFileNames(names) {
255
+ const known = DOTENV_FILE_ORDER.filter((name) => names.includes(name));
256
+ const rest = names
257
+ .filter((name) => name.startsWith(".env.") && !DOTENV_FILE_ORDER.includes(name))
258
+ .sort();
259
+ return [...known, ...rest];
260
+ }
261
+ /**
262
+ * Read the variables of the dotenv files of a workspace.
263
+ *
264
+ * The first file that defines a key wins (see orderDotenvFileNames).
265
+ */
266
+ export function collectDotenvVars(files) {
267
+ const vars = [];
268
+ const seen = new Set();
269
+ for (const file of orderDotenvFileNames(files.map((f) => f.name))) {
270
+ const content = files.find((f) => f.name === file)?.content ?? "";
271
+ for (const variable of parseDotenvVars(content)) {
272
+ if (seen.has(variable.key))
273
+ continue;
274
+ seen.add(variable.key);
275
+ vars.push({ key: variable.key, value: variable.value, file });
276
+ }
277
+ }
278
+ return vars;
279
+ }
280
+ /**
281
+ * The variables of the process env that can name the environment.
282
+ *
283
+ * Only NODE_ENV and the environment shaped keys are collected: an unrelated
284
+ * variable of the process env must not outrank the workspace files.
285
+ */
286
+ export function collectProcessEnvVars(env = process.env) {
287
+ const vars = [];
288
+ for (const key of Object.keys(env)) {
289
+ const value = env[key];
290
+ if (value === undefined)
291
+ continue;
292
+ if (key === "NODE_ENV" || isEnvShapedKey(key)) {
293
+ vars.push({ key, value });
294
+ }
295
+ }
296
+ return vars;
297
+ }
298
+ /**
299
+ * Resolve the workspace mode from the variables that were found.
300
+ *
301
+ * The order is:
302
+ * 1. the NODE_ENV of the process env - it decides, also when it names no
303
+ * known mode (then the mode stays unknown and the label names it: an
304
+ * explicit "staging" must not be overwritten by a file)
305
+ * 2. the dotenv files of the workspace: NODE_ENV, then an environment
306
+ * shaped key WHOSE VALUE names a known mode (e.g. AMOS_ENV=development),
307
+ * then any other environment shaped key (so that the label can name it)
308
+ * 3. an environment shaped key of the process env (the files are silent)
309
+ */
310
+ export function resolveWorkspaceMode(processVars, dotenvVars) {
311
+ const processNodeEnv = processVars.find((v) => v.key === "NODE_ENV");
312
+ if (processNodeEnv) {
313
+ const mode = modeFromValue(processNodeEnv.value);
314
+ if (mode)
315
+ return { mode, hint: processNodeEnv, source: "process.env" };
316
+ // An explicit NODE_ENV that names no known mode decides: the mode
317
+ // stays unknown instead of falling through to the files.
318
+ return { mode: "unknown", hint: processNodeEnv, source: "process.env" };
319
+ }
320
+ const dotenvHint = detectModeHint(dotenvVars, { allowValueMatch: true });
321
+ if (dotenvHint) {
322
+ const mode = modeFromValue(dotenvHint.value);
323
+ if (mode)
324
+ return { mode, hint: dotenvHint, source: ".env file" };
325
+ // The workspace file is explicit - even when its value names no known
326
+ // mode ("APP_MODE=staging"). It must not be replaced by an unrelated
327
+ // variable of the process env.
328
+ return { mode: "unknown", hint: dotenvHint, source: ".env file" };
329
+ }
330
+ // Last resort: an environment shaped key of the process env (the files
331
+ // are silent).
332
+ const processEnvHint = processVars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key));
333
+ if (processEnvHint) {
334
+ const mode = modeFromValue(processEnvHint.value);
335
+ if (mode)
336
+ return { mode, hint: processEnvHint, source: "process.env" };
337
+ return { mode: "unknown", hint: processEnvHint, source: "process.env" };
338
+ }
339
+ return { mode: "unknown", hint: null, source: null };
340
+ }
341
+ /**
342
+ * The evidence entry that names the environment (if any).
343
+ */
344
+ function findModeEvidence(env) {
345
+ const candidates = env.evidence.filter((e) => e.source === "process.env" || e.source === ".env file");
346
+ // A value that names a known mode wins over a value that cannot be read.
347
+ return candidates.find((e) => modeFromValue(e.value) !== null) ?? candidates[0];
348
+ }
97
349
  /**
98
350
  * Human readable mode label.
99
351
  *
100
352
  * "unknown" always carries the reason: the mode can only be proven by an
101
- * explicit NODE_ENV (process env or .env file). When a NODE_ENV IS present
102
- * but names a mode this detector does not know ("staging"), the label says
103
- * exactly that instead of claiming there is no NODE_ENV at all.
353
+ * explicit variable - NODE_ENV, another *_ENV key (e.g. AMOS_ENV) or a
354
+ * value that names a known mode. When such a variable IS present but names
355
+ * a mode this detector does not know ("staging"), the label says exactly
356
+ * that instead of claiming there is no variable at all.
357
+ *
358
+ * A known mode names its source: "DEVELOPMENT (from AMOS_ENV in .env)".
104
359
  */
105
360
  export function describeWorkspaceMode(env) {
361
+ const hint = findModeEvidence(env);
362
+ const where = hint?.file ? ` in ${hint.file}` : "";
106
363
  if (env.mode !== "unknown") {
107
- return env.mode.toUpperCase();
364
+ return hint
365
+ ? `${env.mode.toUpperCase()} (from ${hint.key}${where})`
366
+ : env.mode.toUpperCase();
108
367
  }
109
- const nodeEnv = env.evidence.find((e) => e.key === "NODE_ENV" && (e.source === "process.env" || e.source === ".env file"));
110
- if (nodeEnv) {
111
- return `UNKNOWN (NODE_ENV=${nodeEnv.value} is not a known mode)`;
368
+ if (hint) {
369
+ // The stored mode and the evidence can disagree (a stale cache row):
370
+ // when the value names a mode, that mode is shown - the label must
371
+ // never claim that a known mode is unknown.
372
+ const named = modeFromValue(hint.value);
373
+ if (named)
374
+ return `${named.toUpperCase()} (from ${hint.key}${where})`;
375
+ if (!hint.value)
376
+ return `UNKNOWN (${hint.key} is empty${where})`;
377
+ // A value that was masked (a secret or a dotenv value that names no
378
+ // known mode) is not shown - only the key and the file are.
379
+ if (hint.value === MASKED_MODE_VALUE) {
380
+ const subject = where ? `${hint.key}${where}` : `${hint.key} is set and`;
381
+ return `UNKNOWN (${subject} names no known mode)`;
382
+ }
383
+ return `UNKNOWN (${hint.key}=${hint.value} is not a known mode${where})`;
112
384
  }
113
- return "UNKNOWN (no explicit NODE_ENV)";
385
+ return "UNKNOWN (no NODE_ENV or environment variable found)";
114
386
  }
115
387
  export function describeServerState(stateOrRunning, hasScript) {
116
388
  // The legacy signature (a boolean plus the script flag) is kept so that
@@ -2,22 +2,26 @@
2
2
  * Workspace Environment Detector
3
3
  *
4
4
  * Detects if the workspace is a development environment by checking:
5
- * 1. process.env.NODE_ENV (for build processes)
6
- * 2. .env file existence and NODE_ENV value
5
+ * 1. the NODE_ENV of the process env (for build processes)
6
+ * 2. the dotenv files of the workspace (".env", ".env.*"): NODE_ENV, then
7
+ * an environment shaped key whose VALUE names a known mode (e.g.
8
+ * AMOS_ENV=development), then any other environment shaped key
7
9
  * 3. package.json "dev" script presence
8
10
  *
9
11
  * Stores detected environment in SQLite for agent reference.
10
12
  * Uses synchronous directory creation and exception-safe operations.
11
13
  */
12
- import { describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, formatEnvForAgentResult, formatWorkspacePath } from "./workspace-env-format.js";
13
- import type { EnvEvidence, ServerState, WorkspaceEnvResult, WorkspaceMode } from "./workspace-env-format.js";
14
+ import { collectDotenvVars, collectProcessEnvVars, describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, detectModeHint, formatEnvForAgentResult, formatWorkspacePath, modeFromValue, orderDotenvFileNames, parseDotenvVars, resolveWorkspaceMode } from "./workspace-env-format.js";
15
+ import type { DotenvVar, EnvEvidence, ServerState, WorkspaceEnvResult, WorkspaceMode } from "./workspace-env-format.js";
14
16
  /**
15
17
  * The mode/evidence types and every human readable rendering of the workspace
16
18
  * environment live in workspace-env-format.ts. They are re-exported here so
17
19
  * the existing import paths keep working.
18
20
  */
19
- export { describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, formatEnvForAgentResult, formatWorkspacePath, };
20
- export type { EnvEvidence, ServerState, WorkspaceEnvResult, WorkspaceMode };
21
+ export { collectDotenvVars, collectProcessEnvVars, describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, detectModeHint, formatEnvForAgentResult, formatWorkspacePath, modeFromValue, orderDotenvFileNames, parseDotenvVars, resolveWorkspaceMode, };
22
+ export { canShowModeValue, isEnvShapedKey, isSecretLikeKey, MASKED_MODE_VALUE } from "./workspace-env-format.js";
23
+ export { isTemplateDotenvName, listDotenvFiles, readDotenvFiles } from "./workspace-dotenv.js";
24
+ export type { DotenvVar, EnvEvidence, ServerState, WorkspaceEnvResult, WorkspaceMode };
21
25
  /**
22
26
  * Detect and store workspace environment
23
27
  */