@phnx-labs/agents-cli 1.20.3 → 1.20.5

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 (193) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +48 -17
  3. package/dist/commands/cli.js +1 -1
  4. package/dist/commands/cloud.js +1 -1
  5. package/dist/commands/commands.js +2 -0
  6. package/dist/commands/doctor.js +1 -1
  7. package/dist/commands/exec.js +52 -16
  8. package/dist/commands/hooks.js +6 -6
  9. package/dist/commands/import.js +90 -37
  10. package/dist/commands/inspect.d.ts +26 -0
  11. package/dist/commands/inspect.js +590 -0
  12. package/dist/commands/mcp.js +17 -16
  13. package/dist/commands/models.js +1 -1
  14. package/dist/commands/packages.js +6 -4
  15. package/dist/commands/permissions.js +13 -12
  16. package/dist/commands/plugins.d.ts +13 -0
  17. package/dist/commands/plugins.js +100 -11
  18. package/dist/commands/prune.js +3 -2
  19. package/dist/commands/pull.d.ts +12 -5
  20. package/dist/commands/pull.js +26 -422
  21. package/dist/commands/push.d.ts +14 -0
  22. package/dist/commands/push.js +30 -0
  23. package/dist/commands/repo.d.ts +1 -1
  24. package/dist/commands/repo.js +155 -112
  25. package/dist/commands/resource-view.d.ts +2 -0
  26. package/dist/commands/resource-view.js +12 -3
  27. package/dist/commands/routines.js +32 -7
  28. package/dist/commands/rules.js +1 -1
  29. package/dist/commands/sessions.js +1 -0
  30. package/dist/commands/setup.d.ts +3 -3
  31. package/dist/commands/setup.js +15 -15
  32. package/dist/commands/skills.js +6 -5
  33. package/dist/commands/subagents.js +5 -4
  34. package/dist/commands/sync.d.ts +18 -5
  35. package/dist/commands/sync.js +251 -65
  36. package/dist/commands/teams.js +1 -0
  37. package/dist/commands/tmux.d.ts +25 -0
  38. package/dist/commands/tmux.js +415 -0
  39. package/dist/commands/trash.d.ts +2 -2
  40. package/dist/commands/trash.js +1 -1
  41. package/dist/commands/versions.js +2 -2
  42. package/dist/commands/view.js +14 -4
  43. package/dist/commands/workflows.js +4 -3
  44. package/dist/commands/worktree.d.ts +4 -5
  45. package/dist/commands/worktree.js +4 -4
  46. package/dist/index.js +68 -20
  47. package/dist/lib/agents.d.ts +19 -10
  48. package/dist/lib/agents.js +102 -28
  49. package/dist/lib/auto-pull-worker.d.ts +1 -1
  50. package/dist/lib/auto-pull-worker.js +2 -2
  51. package/dist/lib/auto-pull.d.ts +1 -1
  52. package/dist/lib/auto-pull.js +1 -1
  53. package/dist/lib/beta.d.ts +1 -1
  54. package/dist/lib/beta.js +1 -1
  55. package/dist/lib/capabilities.js +2 -0
  56. package/dist/lib/commands.d.ts +28 -1
  57. package/dist/lib/commands.js +125 -20
  58. package/dist/lib/doctor-diff.js +2 -2
  59. package/dist/lib/exec.d.ts +14 -0
  60. package/dist/lib/exec.js +39 -5
  61. package/dist/lib/fuzzy.d.ts +12 -2
  62. package/dist/lib/fuzzy.js +29 -4
  63. package/dist/lib/git.js +8 -1
  64. package/dist/lib/hooks.d.ts +2 -2
  65. package/dist/lib/hooks.js +97 -10
  66. package/dist/lib/import.d.ts +21 -0
  67. package/dist/lib/import.js +55 -2
  68. package/dist/lib/mcp.js +32 -2
  69. package/dist/lib/migrate.d.ts +51 -0
  70. package/dist/lib/migrate.js +227 -1
  71. package/dist/lib/models.js +62 -15
  72. package/dist/lib/permissions.d.ts +36 -2
  73. package/dist/lib/permissions.js +217 -7
  74. package/dist/lib/plugin-marketplace.d.ts +108 -40
  75. package/dist/lib/plugin-marketplace.js +243 -94
  76. package/dist/lib/plugins.d.ts +21 -4
  77. package/dist/lib/plugins.js +130 -49
  78. package/dist/lib/profiles-presets.js +12 -12
  79. package/dist/lib/project-launch.d.ts +65 -0
  80. package/dist/lib/project-launch.js +367 -0
  81. package/dist/lib/pty-client.js +1 -1
  82. package/dist/lib/pty-server.d.ts +1 -1
  83. package/dist/lib/pty-server.js +28 -4
  84. package/dist/lib/refresh.d.ts +26 -0
  85. package/dist/lib/refresh.js +315 -0
  86. package/dist/lib/resource-patterns.d.ts +1 -1
  87. package/dist/lib/resource-patterns.js +1 -1
  88. package/dist/lib/resources/commands.js +2 -2
  89. package/dist/lib/resources/hooks.d.ts +1 -1
  90. package/dist/lib/resources/hooks.js +1 -1
  91. package/dist/lib/resources/mcp.d.ts +1 -1
  92. package/dist/lib/resources/mcp.js +5 -6
  93. package/dist/lib/resources/permissions.js +5 -2
  94. package/dist/lib/resources/rules.js +3 -2
  95. package/dist/lib/resources/skills.js +3 -2
  96. package/dist/lib/resources/types.d.ts +1 -1
  97. package/dist/lib/resources.js +2 -2
  98. package/dist/lib/rotate.d.ts +1 -1
  99. package/dist/lib/rotate.js +1 -1
  100. package/dist/lib/routines.d.ts +16 -4
  101. package/dist/lib/routines.js +67 -17
  102. package/dist/lib/rules/compile.js +22 -10
  103. package/dist/lib/rules/rules.js +3 -3
  104. package/dist/lib/runner.js +16 -3
  105. package/dist/lib/scheduler.js +15 -1
  106. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  107. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  108. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +9 -1
  109. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  110. package/dist/lib/secrets/linux.d.ts +44 -9
  111. package/dist/lib/secrets/linux.js +302 -48
  112. package/dist/lib/session/db.js +15 -2
  113. package/dist/lib/session/discover.js +118 -3
  114. package/dist/lib/session/parse.js +3 -0
  115. package/dist/lib/session/types.d.ts +1 -1
  116. package/dist/lib/session/types.js +1 -1
  117. package/dist/lib/shims.d.ts +10 -9
  118. package/dist/lib/shims.js +101 -50
  119. package/dist/lib/skills.d.ts +1 -1
  120. package/dist/lib/skills.js +10 -9
  121. package/dist/lib/staleness/detectors/commands.d.ts +3 -0
  122. package/dist/lib/staleness/detectors/commands.js +46 -0
  123. package/dist/lib/staleness/detectors/hooks.d.ts +3 -0
  124. package/dist/lib/staleness/detectors/hooks.js +44 -0
  125. package/dist/lib/staleness/detectors/mcp.d.ts +3 -0
  126. package/dist/lib/staleness/detectors/mcp.js +31 -0
  127. package/dist/lib/staleness/detectors/permissions.d.ts +3 -0
  128. package/dist/lib/staleness/detectors/permissions.js +201 -0
  129. package/dist/lib/staleness/detectors/plugins.d.ts +8 -0
  130. package/dist/lib/staleness/detectors/plugins.js +23 -0
  131. package/dist/lib/staleness/detectors/rules.d.ts +3 -0
  132. package/dist/lib/staleness/detectors/rules.js +34 -0
  133. package/dist/lib/staleness/detectors/skills.d.ts +3 -0
  134. package/dist/lib/staleness/detectors/skills.js +71 -0
  135. package/dist/lib/staleness/detectors/subagents.d.ts +3 -0
  136. package/dist/lib/staleness/detectors/subagents.js +50 -0
  137. package/dist/lib/staleness/detectors/types.d.ts +22 -0
  138. package/dist/lib/staleness/detectors/types.js +1 -0
  139. package/dist/lib/staleness/detectors/workflows.d.ts +3 -0
  140. package/dist/lib/staleness/detectors/workflows.js +28 -0
  141. package/dist/lib/staleness/registry.d.ts +26 -0
  142. package/dist/lib/staleness/registry.js +123 -0
  143. package/dist/lib/staleness/writers/commands.d.ts +3 -0
  144. package/dist/lib/staleness/writers/commands.js +111 -0
  145. package/dist/lib/staleness/writers/hooks.d.ts +3 -0
  146. package/dist/lib/staleness/writers/hooks.js +47 -0
  147. package/dist/lib/staleness/writers/kinds.d.ts +10 -0
  148. package/dist/lib/staleness/writers/kinds.js +15 -0
  149. package/dist/lib/staleness/writers/lazy-map.d.ts +13 -0
  150. package/dist/lib/staleness/writers/lazy-map.js +19 -0
  151. package/dist/lib/staleness/writers/mcp.d.ts +10 -0
  152. package/dist/lib/staleness/writers/mcp.js +19 -0
  153. package/dist/lib/staleness/writers/permissions.d.ts +13 -0
  154. package/dist/lib/staleness/writers/permissions.js +26 -0
  155. package/dist/lib/staleness/writers/plugins.d.ts +7 -0
  156. package/dist/lib/staleness/writers/plugins.js +31 -0
  157. package/dist/lib/staleness/writers/rules.d.ts +7 -0
  158. package/dist/lib/staleness/writers/rules.js +55 -0
  159. package/dist/lib/staleness/writers/skills.d.ts +3 -0
  160. package/dist/lib/staleness/writers/skills.js +81 -0
  161. package/dist/lib/staleness/writers/sources.d.ts +16 -0
  162. package/dist/lib/staleness/writers/sources.js +72 -0
  163. package/dist/lib/staleness/writers/subagents.d.ts +3 -0
  164. package/dist/lib/staleness/writers/subagents.js +53 -0
  165. package/dist/lib/staleness/writers/types.d.ts +36 -0
  166. package/dist/lib/staleness/writers/types.js +1 -0
  167. package/dist/lib/staleness/writers/workflows.d.ts +7 -0
  168. package/dist/lib/staleness/writers/workflows.js +31 -0
  169. package/dist/lib/state.d.ts +34 -11
  170. package/dist/lib/state.js +58 -13
  171. package/dist/lib/subagents.d.ts +0 -2
  172. package/dist/lib/subagents.js +6 -6
  173. package/dist/lib/teams/agents.js +1 -1
  174. package/dist/lib/teams/parsers.d.ts +1 -1
  175. package/dist/lib/tmux/binary.d.ts +67 -0
  176. package/dist/lib/tmux/binary.js +141 -0
  177. package/dist/lib/tmux/index.d.ts +8 -0
  178. package/dist/lib/tmux/index.js +8 -0
  179. package/dist/lib/tmux/paths.d.ts +17 -0
  180. package/dist/lib/tmux/paths.js +30 -0
  181. package/dist/lib/tmux/session.d.ts +122 -0
  182. package/dist/lib/tmux/session.js +305 -0
  183. package/dist/lib/types.d.ts +58 -7
  184. package/dist/lib/types.js +1 -1
  185. package/dist/lib/usage.js +1 -1
  186. package/dist/lib/versions.d.ts +4 -4
  187. package/dist/lib/versions.js +154 -491
  188. package/dist/lib/workflows.d.ts +2 -4
  189. package/dist/lib/workflows.js +3 -4
  190. package/package.json +7 -7
  191. package/scripts/postinstall.js +16 -63
  192. package/dist/commands/status.d.ts +0 -9
  193. package/dist/commands/status.js +0 -25
@@ -10,7 +10,7 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as yaml from 'yaml';
12
12
  import { Cron } from 'croner';
13
- import { getRoutinesDir, getRunsDir, ensureAgentsDir } from './state.js';
13
+ import { getRoutinesDir, getRunsDir, ensureAgentsDir, getProjectRoutinesDir } from './state.js';
14
14
  import { safeJoin } from './paths.js';
15
15
  import { ALL_AGENT_IDS } from './agents.js';
16
16
  /** Default values applied to every job config when fields are omitted. */
@@ -20,29 +20,59 @@ const JOB_DEFAULTS = {
20
20
  timeout: '10m',
21
21
  enabled: true,
22
22
  };
23
- /** List all job configs from ~/.agents/routines/. */
24
- export function listJobs() {
23
+ /**
24
+ * List all job configs, scanning project > user routine dirs.
25
+ * Project routines (`<project>/.agents/routines/`) shadow user routines of the
26
+ * same name. Project discovery is opt-in via `cwd`; the daemon (which calls
27
+ * `listJobs()` with no argument) only sees user routines.
28
+ */
29
+ export function listJobs(cwd) {
25
30
  ensureAgentsDir();
26
- const jobsDir = getRoutinesDir();
27
- if (!fs.existsSync(jobsDir))
28
- return [];
29
- const files = fs.readdirSync(jobsDir).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
31
+ const seen = new Set();
30
32
  const jobs = [];
31
- for (const file of files) {
32
- const job = readJobFile(path.join(jobsDir, file));
33
- if (job)
33
+ const dirs = [];
34
+ if (cwd) {
35
+ const projectDir = getProjectRoutinesDir(cwd);
36
+ if (projectDir)
37
+ dirs.push(projectDir);
38
+ }
39
+ dirs.push(getRoutinesDir());
40
+ for (const dir of dirs) {
41
+ if (!fs.existsSync(dir))
42
+ continue;
43
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
44
+ for (const file of files) {
45
+ const job = readJobFile(path.join(dir, file));
46
+ if (!job)
47
+ continue;
48
+ if (seen.has(job.name))
49
+ continue;
50
+ seen.add(job.name);
34
51
  jobs.push(job);
52
+ }
35
53
  }
36
54
  return jobs;
37
55
  }
38
- /** Read a single job config by name. Returns null if not found. */
39
- export function readJob(name) {
56
+ /**
57
+ * Read a single job config by name, checking project > user.
58
+ * Project discovery is opt-in via `cwd`; daemon callers pass no argument and
59
+ * only resolve user routines.
60
+ */
61
+ export function readJob(name, cwd) {
40
62
  ensureAgentsDir();
41
- const jobsDir = getRoutinesDir();
42
- for (const ext of ['.yml', '.yaml']) {
43
- const filePath = safeJoin(jobsDir, name + ext);
44
- if (fs.existsSync(filePath)) {
45
- return readJobFile(filePath);
63
+ const dirs = [];
64
+ if (cwd) {
65
+ const projectDir = getProjectRoutinesDir(cwd);
66
+ if (projectDir)
67
+ dirs.push(projectDir);
68
+ }
69
+ dirs.push(getRoutinesDir());
70
+ for (const dir of dirs) {
71
+ for (const ext of ['.yml', '.yaml']) {
72
+ const filePath = safeJoin(dir, name + ext);
73
+ if (fs.existsSync(filePath)) {
74
+ return readJobFile(filePath);
75
+ }
46
76
  }
47
77
  }
48
78
  return null;
@@ -147,8 +177,28 @@ export function validateJob(config) {
147
177
  if (config.timeout && !parseTimeout(config.timeout)) {
148
178
  errors.push('timeout must be like 10m, 2h, 3d, 1w (max 1w)');
149
179
  }
180
+ if (config.endAt !== undefined) {
181
+ if (typeof config.endAt !== 'string' || !isParseableDate(config.endAt)) {
182
+ errors.push('endAt must be a parseable ISO 8601 / RFC3339 timestamp (e.g., 2026-12-31T23:59:00Z)');
183
+ }
184
+ }
150
185
  return errors;
151
186
  }
187
+ function isParseableDate(value) {
188
+ if (!value.trim())
189
+ return false;
190
+ const ts = Date.parse(value);
191
+ return Number.isFinite(ts);
192
+ }
193
+ /** True when a job's endAt has already elapsed. False when endAt is unset or in the future. */
194
+ export function isPastEndAt(config, now = new Date()) {
195
+ if (!config.endAt)
196
+ return false;
197
+ const end = Date.parse(config.endAt);
198
+ if (!Number.isFinite(end))
199
+ return false;
200
+ return now.getTime() >= end;
201
+ }
152
202
  /** Expand built-in and user-defined template variables in a job's prompt string. */
153
203
  export function resolveJobPrompt(config) {
154
204
  const now = new Date();
@@ -10,8 +10,8 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as os from 'os';
12
12
  import * as crypto from 'crypto';
13
- import { AGENTS } from '../agents.js';
14
- import { getResolvedRulesDir, getVersionsDir } from '../state.js';
13
+ import { AGENTS, agentConfigDirName } from '../agents.js';
14
+ import { getVersionsDir } from '../state.js';
15
15
  import { composeRules, composeRulesFromState } from './compose.js';
16
16
  // Match `@path` preceded by start-of-string or whitespace. This avoids
17
17
  // matching emails ("foo@bar.com") and the middle of words. The leading
@@ -97,7 +97,7 @@ export function supportsRulesImports(agentId) {
97
97
  function getCompiledRulesPath(agentId, version) {
98
98
  const agentConfig = AGENTS[agentId];
99
99
  const versionHome = path.join(getVersionsDir(), agentId, version, 'home');
100
- return path.join(versionHome, `.${agentId}`, agentConfig.instructionsFile);
100
+ return path.join(versionHome, agentConfigDirName(agentId), agentConfig.instructionsFile);
101
101
  }
102
102
  function getManifestPath(compiledPath) {
103
103
  return compiledPath + '.manifest.json';
@@ -151,14 +151,24 @@ export function compileRulesForAgent(agentId, version) {
151
151
  if (supportsRulesImports(agentId)) {
152
152
  return { compiled: false, compiledPath: '', sources: 0 };
153
153
  }
154
- const rulesDir = getResolvedRulesDir();
155
- const sourceAgents = path.join(rulesDir, 'AGENTS.md');
156
- if (!fs.existsSync(sourceAgents)) {
154
+ // Route through the layered composer (project > user > extras > system).
155
+ // The previous implementation read only `<systemRules>/AGENTS.md` and
156
+ // inlined its @-imports — that dropped user/extras/project subrules
157
+ // entirely for @-import-incapable agents (Cursor, older Codex), so
158
+ // updates to ~/.agents/rules/subrules/ never reached the version home.
159
+ // composeRulesFromState already returns the concatenated content with
160
+ // every fragment resolved across layers; we just need to record the
161
+ // composed source list for staleness detection.
162
+ let composed;
163
+ try {
164
+ composed = composeRulesFromState({ preset: undefined });
165
+ }
166
+ catch {
167
+ // No rules.yaml in any layer, or the default preset is missing — leave
168
+ // the version home untouched (matches the previous file-missing branch).
157
169
  return { compiled: false, compiledPath: '', sources: 0 };
158
170
  }
159
- const rootContent = fs.readFileSync(sourceAgents, 'utf8');
160
- const { content, sources } = resolveImports(rootContent, rulesDir);
161
- const newContent = COMPILED_HEADER + content;
171
+ const newContent = COMPILED_HEADER + composed.content;
162
172
  const compiledPath = getCompiledRulesPath(agentId, version);
163
173
  fs.mkdirSync(path.dirname(compiledPath), { recursive: true });
164
174
  const existing = fs.existsSync(compiledPath) ? fs.readFileSync(compiledPath, 'utf8') : null;
@@ -166,7 +176,9 @@ export function compileRulesForAgent(agentId, version) {
166
176
  return { compiled: false, compiledPath, sources: 0 };
167
177
  }
168
178
  fs.writeFileSync(compiledPath, newContent);
169
- const allSources = [sourceAgents, ...sources];
179
+ // Track every concrete subrule file the composer included as a source for
180
+ // staleness. composeRulesFromState exposes sourcePath on each ComposedSubrule.
181
+ const allSources = composed.subrules.map(s => s.sourcePath);
170
182
  const manifest = {
171
183
  compiledAt: new Date().toISOString(),
172
184
  sources: allSources.map(p => {
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
- import { AGENTS, ALL_AGENT_IDS } from '../agents.js';
11
+ import { AGENTS, ALL_AGENT_IDS, agentConfigDirName } from '../agents.js';
12
12
  import { getResolvedRulesDir, getUserRulesDir, getProjectAgentsDir } from '../state.js';
13
13
  import { getEffectiveHome } from '../versions.js';
14
14
  /**
@@ -65,7 +65,7 @@ function normalizeContent(content) {
65
65
  */
66
66
  function getUserConfigDir(agentId) {
67
67
  const home = getEffectiveHome(agentId);
68
- return path.join(home, `.${agentId}`);
68
+ return path.join(home, agentConfigDirName(agentId));
69
69
  }
70
70
  export function getInstructionsPath(agentId, scope, cwd = process.cwd()) {
71
71
  const agent = AGENTS[agentId];
@@ -211,7 +211,7 @@ export function listInstalledInstructionsWithScope(agentId, cwd = process.cwd(),
211
211
  const agent = AGENTS[agentId];
212
212
  // User-scoped instructions (version-aware when home is provided)
213
213
  const home = options?.home || getEffectiveHome(agentId);
214
- const userConfigDir = path.join(home, `.${agentId}`);
214
+ const userConfigDir = path.join(home, agentConfigDirName(agentId));
215
215
  const userPath = path.join(userConfigDir, agent.instructionsFile);
216
216
  results.push({
217
217
  agentId,
@@ -21,6 +21,7 @@ const AGENT_COMMANDS = {
21
21
  claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
22
22
  codex: ['codex', 'exec', '--sandbox', 'workspace-write', '{prompt}', '--json'],
23
23
  gemini: ['gemini', '{prompt}', '--output-format', 'stream-json'],
24
+ kimi: ['kimi', '--prompt', '{prompt}', '--output-format', 'stream-json'],
24
25
  };
25
26
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
26
27
  export function buildJobCommand(config, resolvedPrompt) {
@@ -72,14 +73,14 @@ export function buildJobCommand(config, resolvedPrompt) {
72
73
  }
73
74
  if (config.agent === 'codex') {
74
75
  if (mode === 'edit') {
75
- cmd.push('--full-auto');
76
+ cmd.push('--dangerously-bypass-approvals-and-sandbox');
76
77
  }
77
78
  else if (mode === 'skip') {
78
- // Remove sandbox restriction, just --full-auto
79
+ // Remove sandbox restriction, just --dangerously-bypass-approvals-and-sandbox
79
80
  const sbIndex = cmd.indexOf('--sandbox');
80
81
  if (sbIndex !== -1)
81
82
  cmd.splice(sbIndex, 2);
82
- cmd.push('--full-auto');
83
+ cmd.push('--dangerously-bypass-approvals-and-sandbox');
83
84
  }
84
85
  appendModelAndReasoning(cmd, config);
85
86
  }
@@ -92,6 +93,18 @@ export function buildJobCommand(config, resolvedPrompt) {
92
93
  }
93
94
  appendModelAndReasoning(cmd, config);
94
95
  }
96
+ if (config.agent === 'kimi') {
97
+ if (mode === 'plan') {
98
+ cmd.push('--plan');
99
+ }
100
+ else if (mode === 'auto') {
101
+ cmd.push('--auto');
102
+ }
103
+ else if (mode === 'skip') {
104
+ cmd.push('--yolo');
105
+ }
106
+ appendModelAndReasoning(cmd, config);
107
+ }
95
108
  return cmd;
96
109
  }
97
110
  /**
@@ -6,7 +6,7 @@
6
6
  * on startup and reloads them on SIGHUP.
7
7
  */
8
8
  import { Cron } from 'croner';
9
- import { listJobs, deleteJob } from './routines.js';
9
+ import { listJobs, deleteJob, isPastEndAt, setJobEnabled } from './routines.js';
10
10
  /** In-memory cron scheduler that triggers a callback when jobs fire. */
11
11
  export class JobScheduler {
12
12
  jobs = new Map();
@@ -32,6 +32,20 @@ export class JobScheduler {
32
32
  if (config.timezone)
33
33
  cronOptions.timezone = config.timezone;
34
34
  const cron = new Cron(config.schedule, cronOptions, async () => {
35
+ // endAt: once the configured end time has passed, auto-disable and stop
36
+ // firing. We persist enabled=false to disk so the next daemon reload
37
+ // doesn't re-schedule, and unschedule in-memory so this cron stops.
38
+ if (isPastEndAt(config)) {
39
+ this.unschedule(config.name);
40
+ try {
41
+ setJobEnabled(config.name, false);
42
+ }
43
+ catch (err) {
44
+ console.error(`Job '${config.name}' endAt auto-disable failed:`, err.message);
45
+ }
46
+ console.log(`Job '${config.name}' reached endAt (${config.endAt}); auto-disabled.`);
47
+ return;
48
+ }
35
49
  try {
36
50
  await this.onTrigger(config);
37
51
  }
@@ -5,7 +5,15 @@
5
5
  <key>files</key>
6
6
  <dict/>
7
7
  <key>files2</key>
8
- <dict/>
8
+ <dict>
9
+ <key>embedded.provisionprofile</key>
10
+ <dict>
11
+ <key>hash2</key>
12
+ <data>
13
+ 2vfA/eR3dTYgNc/fXhdADUPkp5tRIepPzE3FCLfDx4w=
14
+ </data>
15
+ </dict>
16
+ </dict>
9
17
  <key>rules</key>
10
18
  <dict>
11
19
  <key>^Resources/</key>
@@ -1,19 +1,44 @@
1
1
  /**
2
2
  * Linux secret storage via libsecret (GNOME Keyring / Secret Service API).
3
3
  *
4
- * Uses `secret-tool` CLI which is part of libsecret-tools package.
5
- * On Ubuntu: apt install libsecret-tools
4
+ * Primary backend: `secret-tool` CLI (libsecret-tools package).
6
5
  *
7
- * Secrets are stored with:
6
+ * Headless fallback: when the default Secret Service collection is locked
7
+ * (common on server-class Linux — no graphical login means the keyring
8
+ * passphrase never enters the daemon, so `secret-tool store` fails with
9
+ * "Cannot create an item in a locked collection"), we transparently switch
10
+ * to a file-based AES-256-GCM encrypted store under
11
+ * `~/.agents/.cache/secrets/`. The encryption key is scrypt-derived from a
12
+ * passphrase read from `AGENTS_SECRETS_PASSPHRASE` (preferred) or a TTY
13
+ * prompt. The decision is cached per process; one stderr line is emitted
14
+ * the first time the fallback activates.
15
+ *
16
+ * Secrets stored via secret-tool use:
8
17
  * service = "agents-cli"
9
18
  * account = username
10
- * item = the secret identifier
19
+ * item = the secret identifier
20
+ *
21
+ * File-fallback layout: one `<item>.enc` JSON file per item, mode 0600.
11
22
  */
12
23
  import type { KeychainBackend } from './index.js';
13
- /**
14
- * secret-tool lookup attributes:
15
- * service=agents-cli account=<user> item=<itemName>
16
- */
24
+ /** Encrypted-file on-disk shape. Exported for tests. */
25
+ export interface EncFile {
26
+ salt: string;
27
+ iv: string;
28
+ authTag: string;
29
+ ciphertext: string;
30
+ }
31
+ /** Encrypt plaintext under a passphrase using AES-256-GCM with a random
32
+ * scrypt salt and a random 96-bit IV. Exported for tests. */
33
+ export declare function encryptForFallback(plaintext: string, passphrase: string): EncFile;
34
+ /** Decrypt an EncFile under a passphrase. Throws on wrong key or tampered
35
+ * ciphertext (auth-tag mismatch). Exported for tests. */
36
+ export declare function decryptForFallback(enc: EncFile, passphrase: string): string;
37
+ /** File-only KeychainBackend (exported for tests; the public surface uses
38
+ * the secret-tool-with-fallback `linuxBackend` below). */
39
+ export declare const fileBackend: KeychainBackend;
40
+ /** secret-tool lookup attributes:
41
+ * service=agents-cli account=<user> item=<itemName> */
17
42
  export declare function hasSecretToolToken(item: string): boolean;
18
43
  export declare function getSecretToolToken(item: string): string;
19
44
  export declare function setSecretToolToken(item: string, value: string): void;
@@ -23,5 +48,15 @@ export declare function deleteSecretToolToken(item: string): boolean;
23
48
  * so we use secret-tool search which outputs in a specific format.
24
49
  */
25
50
  export declare function listSecretToolItems(prefix: string): string[];
26
- /** KeychainBackend implementation for Linux using secret-tool */
51
+ /** KeychainBackend implementation for Linux. Routes through secret-tool
52
+ * with a transparent encrypted-file fallback when the default Secret
53
+ * Service collection is locked (or libsecret-tools is not installed but
54
+ * AGENTS_SECRETS_PASSPHRASE is set). */
27
55
  export declare const linuxBackend: KeychainBackend;
56
+ /** Test-only: reset module state so independent test cases don't bleed
57
+ * passphrase / fallback decisions across each other. */
58
+ export declare function _resetForTest(opts?: {
59
+ fileDir?: string | null;
60
+ forceFileFallback?: boolean;
61
+ passphrase?: string | null;
62
+ }): void;