@codyswann/lisa 4.81.2 → 4.81.4

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 (190) hide show
  1. package/dist/core/lisa.d.ts.map +1 -1
  2. package/dist/core/lisa.js +3 -0
  3. package/dist/core/lisa.js.map +1 -1
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +47 -8
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/dist/strategies/copy-contents.d.ts.map +1 -1
  9. package/dist/strategies/copy-contents.js +3 -1
  10. package/dist/strategies/copy-contents.js.map +1 -1
  11. package/dist/strategies/copy-overwrite.d.ts.map +1 -1
  12. package/dist/strategies/copy-overwrite.js +3 -0
  13. package/dist/strategies/copy-overwrite.js.map +1 -1
  14. package/dist/strategies/create-only.d.ts.map +1 -1
  15. package/dist/strategies/create-only.js +8 -0
  16. package/dist/strategies/create-only.js.map +1 -1
  17. package/dist/strategies/merge.d.ts.map +1 -1
  18. package/dist/strategies/merge.js +2 -0
  19. package/dist/strategies/merge.js.map +1 -1
  20. package/dist/strategies/package-lisa.d.ts.map +1 -1
  21. package/dist/strategies/package-lisa.js +2 -0
  22. package/dist/strategies/package-lisa.js.map +1 -1
  23. package/dist/strategies/strategy.interface.d.ts +12 -1
  24. package/dist/strategies/strategy.interface.d.ts.map +1 -1
  25. package/dist/strategies/tagged-merge.d.ts.map +1 -1
  26. package/dist/strategies/tagged-merge.js +2 -0
  27. package/dist/strategies/tagged-merge.js.map +1 -1
  28. package/dist/transaction/backup.d.ts +27 -1
  29. package/dist/transaction/backup.d.ts.map +1 -1
  30. package/dist/transaction/backup.js +65 -3
  31. package/dist/transaction/backup.js.map +1 -1
  32. package/dist/transaction/created-paths.d.ts +72 -0
  33. package/dist/transaction/created-paths.d.ts.map +1 -0
  34. package/dist/transaction/created-paths.js +176 -0
  35. package/dist/transaction/created-paths.js.map +1 -0
  36. package/package.json +4 -4
  37. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +2 -2
  40. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  41. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  42. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
  43. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  44. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +18 -36
  45. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  46. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  47. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  48. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  49. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token.mjs +98 -28
  50. package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +44 -21
  51. package/plugins/lisa/skills/lisa-session-manager/SKILL.md +2 -2
  52. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  53. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  54. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
  55. package/plugins/lisa/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  56. package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +18 -36
  57. package/plugins/lisa/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  58. package/plugins/lisa/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  59. package/plugins/lisa/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  60. package/plugins/lisa/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  61. package/plugins/lisa/skills/lisa-session-manager/scripts/token.mjs +98 -28
  62. package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +44 -21
  63. package/plugins/lisa-agy/plugin.json +1 -1
  64. package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +2 -2
  65. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  66. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  67. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
  68. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  69. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +18 -36
  70. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  71. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  72. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  73. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  74. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token.mjs +98 -28
  75. package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +44 -21
  76. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  79. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  83. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +2 -2
  85. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  86. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  87. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
  88. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  89. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +18 -36
  90. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  91. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  92. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  93. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  94. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token.mjs +98 -28
  95. package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +44 -21
  96. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +2 -2
  98. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  99. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  100. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
  101. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  102. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +18 -36
  103. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  104. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  105. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  106. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  107. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token.mjs +98 -28
  108. package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +44 -21
  109. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  111. package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +2 -2
  112. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  113. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  114. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
  115. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  116. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +18 -36
  117. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  118. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  119. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  120. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  121. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token.mjs +98 -28
  122. package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +44 -21
  123. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-expo-agy/plugin.json +1 -1
  126. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  130. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  132. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  133. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  134. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  135. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  136. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  137. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  139. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  140. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  141. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  142. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  143. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  144. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  145. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  146. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  147. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  148. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  149. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  150. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  151. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  153. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  154. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  155. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  156. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  158. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  159. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  160. package/plugins/lisa-rails-agy/plugin.json +1 -1
  161. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  163. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  164. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  165. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  166. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  167. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  168. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  169. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  170. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  171. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  172. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  173. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  174. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  175. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  176. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  177. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  178. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  179. package/plugins/src/base/skills/lisa-session-manager/SKILL.md +2 -2
  180. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  181. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  182. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
  183. package/plugins/src/base/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  184. package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +18 -36
  185. package/plugins/src/base/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  186. package/plugins/src/base/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  187. package/plugins/src/base/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  188. package/plugins/src/base/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  189. package/plugins/src/base/skills/lisa-session-manager/scripts/token.mjs +98 -28
  190. package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +44 -21
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Setup subcommands of the session-manager CLI: init-config, store-token,
3
+ * trust-status and coexistence. Each one answers a question
4
+ * `lisa-setup-session-manager` would otherwise put to the operator, so setup
5
+ * asks only what a script cannot find out.
6
+ * @module session-manager/cli-setup
7
+ */
8
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
+ import path from "node:path";
10
+
11
+ import {
12
+ CliError,
13
+ EXIT,
14
+ GLOBAL_CONFIG_FILE,
15
+ asSetupError,
16
+ asSetupErrorAsync,
17
+ loadProject,
18
+ requireConfig,
19
+ } from "./cli-runtime.mjs";
20
+ import {
21
+ buildDefaultBlock,
22
+ detectRepos,
23
+ draftHints,
24
+ resolveRepoDir,
25
+ } from "./config.mjs";
26
+ import { defaultProjectSlug } from "./isolation.mjs";
27
+ import {
28
+ storeToken as storeTokenFile,
29
+ storeTokenFromClipboard,
30
+ } from "./launch.mjs";
31
+ import {
32
+ coexistence as readCoexistence,
33
+ trustStatus as readTrust,
34
+ } from "./setup-checks.mjs";
35
+
36
+ /**
37
+ * `init-config [--slug <slug>] [--write]`: the default block for this
38
+ * project, written to `.lisa.config.json` only with `--write` and only when
39
+ * no block exists. Non-secret fields only; the token never goes here. Repos
40
+ * are the routable children `detectRepos` finds (linked worktrees and second
41
+ * clones of one remote left out), each with drafted hints.
42
+ * @param {object} ctx - CLI context
43
+ * @returns {{ json: object }} `{written,block,...}`
44
+ */
45
+ export function initConfig(ctx) {
46
+ const { global } = loadProject(ctx.root);
47
+ if (global?.sessionManager) {
48
+ return {
49
+ json: { written: false, reason: "exists", block: global.sessionManager },
50
+ };
51
+ }
52
+ const slug =
53
+ ctx.values.slug ?? defaultProjectSlug(ctx.root, global?.github?.repo);
54
+ const detectedRepos = detectRepos(ctx.root);
55
+ const hints = Object.fromEntries(
56
+ detectedRepos.map(name => [
57
+ name,
58
+ draftHints(path.join(ctx.root, "projects", name), name),
59
+ ])
60
+ );
61
+ const block = asSetupError(() =>
62
+ buildDefaultBlock({ projectSlug: slug, detectedRepos, hints })
63
+ );
64
+ if (!ctx.values.write) return { json: { written: false, block } };
65
+
66
+ const file = path.join(ctx.root, GLOBAL_CONFIG_FILE);
67
+ if (!existsSync(file)) {
68
+ throw new CliError(
69
+ `${file} does not exist; set up Lisa in this project first.`,
70
+ EXIT.SETUP
71
+ );
72
+ }
73
+ const current = JSON.parse(readFileSync(file, "utf8"));
74
+ writeFileSync(
75
+ file,
76
+ `${JSON.stringify({ ...current, sessionManager: block }, null, 2)}\n`
77
+ );
78
+ return { json: { written: true, path: file, block } };
79
+ }
80
+
81
+ /**
82
+ * `store-token [--replace] [--from-clipboard]`: store the bot token in the
83
+ * project's state dir. By default it prompts on the operator's terminal (echo
84
+ * off); with no TTY it prints the two commands that work instead and exits 0
85
+ * with `stored:false`. `--from-clipboard` reads the system clipboard and needs
86
+ * no TTY. An existing token is kept (exit 2) unless `--replace` is given.
87
+ * @param {object} ctx - CLI context
88
+ * @returns {Promise<{ json: object, exitCode: number }>} Outcome
89
+ */
90
+ export async function storeToken(ctx) {
91
+ const config = requireConfig(ctx);
92
+ const input = {
93
+ stateDir: config.notify?.stateDir,
94
+ scriptsDir: ctx.scriptsDir,
95
+ projectRoot: ctx.root,
96
+ replace: ctx.values.replace === true,
97
+ env: ctx.env,
98
+ homedir: ctx.homedir,
99
+ };
100
+ const result = ctx.values["from-clipboard"]
101
+ ? asSetupError(() => storeTokenFromClipboard(input))
102
+ : await asSetupErrorAsync(() => storeTokenFile(input));
103
+ const refused = result.stored === false && result.reason !== "no-tty";
104
+ return { json: result, exitCode: refused ? EXIT.SETUP : EXIT.OK };
105
+ }
106
+
107
+ /**
108
+ * Every directory a worker can start in: each routed repo, or the root on a
109
+ * single-repo project.
110
+ * @param {object} ctx - CLI context
111
+ * @param {object} config - Resolved block
112
+ * @returns {string[]} Absolute directories
113
+ */
114
+ function workerDirs(ctx, config) {
115
+ const keys = Object.keys(config.repos?.entries ?? {});
116
+ if (keys.length === 0) return [ctx.root];
117
+ return keys.map(key =>
118
+ asSetupError(() => resolveRepoDir(config, key, ctx.root))
119
+ );
120
+ }
121
+
122
+ /**
123
+ * `trust-status`: whether Claude Code has recorded workspace trust for each
124
+ * worker directory. Exit 0 either way; setup asks the operator only about the
125
+ * rows that are not `trusted: true`.
126
+ * @param {object} ctx - CLI context
127
+ * @returns {{ json: object }} `{file,dirs:[{dir,trusted}],needsTrust}`
128
+ */
129
+ export function trustStatus(ctx) {
130
+ const config = requireConfig(ctx);
131
+ const report = readTrust({
132
+ dirs: workerDirs(ctx, config),
133
+ env: ctx.env,
134
+ homedir: ctx.homedir,
135
+ });
136
+ return {
137
+ json: {
138
+ ...report,
139
+ needsTrust: report.dirs
140
+ .filter(row => row.trusted !== true)
141
+ .map(row => row.dir),
142
+ },
143
+ };
144
+ }
145
+
146
+ /**
147
+ * `coexistence`: what this project's run logs say about the scheduled intake
148
+ * loops that compete with the manager. Exit 0 either way.
149
+ * @param {object} ctx - CLI context
150
+ * @returns {{ json: object }} `{windowHours,loops:[...],live}`
151
+ */
152
+ export function coexistence(ctx) {
153
+ const config = requireConfig(ctx);
154
+ const report = readCoexistence({
155
+ root: ctx.root,
156
+ now: ctx.now(),
157
+ stall: config.stall,
158
+ });
159
+ return {
160
+ json: {
161
+ ...report,
162
+ live: report.loops.filter(row => row.recent).map(row => row.loop),
163
+ },
164
+ };
165
+ }
@@ -61,12 +61,19 @@ Manager
61
61
  stop tmux kill-session -t =<name> only, and only for a session started
62
62
  in this root (exit 2 otherwise); workers keep running.
63
63
  stdout {status:"stopped"|"not-running",name}.
64
- store-token [--replace]
64
+ store-token [--replace] [--from-clipboard]
65
65
  Prompts for the bot token on the terminal (echo off) and writes
66
66
  <stateDir>/.env. Exit 2 when a token is already there, unless
67
- --replace. With no TTY: exit 0, stdout {stored:false,
68
- reason:"no-tty",command} where command is the line the operator
69
- runs in their own terminal. Bad input: exit 2.
67
+ --replace. With no TTY (an agent's Bash, and Claude Code's !
68
+ prefix too): exit 0, stdout {stored:false,reason:"no-tty",
69
+ message,command,clipboardCommand}; command is the line the
70
+ operator runs in a separate terminal app.
71
+ --from-clipboard reads the token from the clipboard instead
72
+ (pbpaste; wl-paste, xclip or xsel on Linux), needs no TTY, and
73
+ clears the clipboard after a successful store. stdout
74
+ {stored:true,path,clipboardCleared}. The token is never printed.
75
+ Bad input, empty clipboard or no clipboard tool: exit 2,
76
+ {stored:false,reason,message}.
70
77
 
71
78
  Config and routing
72
79
  repo-dir [--repo <key>] [--raw]
@@ -75,10 +82,27 @@ Config and routing
75
82
  validate stdout {valid,errors}; exit 2 when invalid or absent.
76
83
  init-config [--slug <slug>] [--write]
77
84
  stdout {written,block[,reason:"exists"][,path]}. Detects
78
- projects/*/.lisa.config.json. Default slug: <github.repo or
79
- folder name>-<first 6 hex of sha256(realpath(root))>; --slug is
80
- used as given. --write adds the block to .lisa.config.json,
81
- never overwriting an existing one.
85
+ projects/*/.lisa.config.json, skipping linked worktrees (.git is
86
+ a file) and keeping one child per origin remote (the one named
87
+ after the repo, else the first by name). Each repo's hints are a
88
+ draft: its name, then one per well-known package.json dependency
89
+ (e.g. @nestjs/* -> NestJS, expo -> Expo, aws-cdk-lib -> AWS CDK).
90
+ Default slug: <github.repo or folder name>-<first 6 hex of
91
+ sha256(realpath(root))>; --slug is used as given. --write adds
92
+ the block to .lisa.config.json, never overwriting an existing one.
93
+ trust-status
94
+ stdout {file,dirs:[{dir,trusted:true|false|"unknown"}],
95
+ needsTrust:[dir]}: whether Claude Code has recorded workspace
96
+ trust (hasTrustDialogAccepted in $CLAUDE_CONFIG_DIR/.claude.json,
97
+ else ~/.claude.json) for each worker dir. "unknown" when that
98
+ file cannot be read. Read-only; never accepts trust.
99
+ coexistence
100
+ stdout {windowHours,loops:[{loop,log,lastRun:{ts,outcome}|null,
101
+ recent}],live:[loop]} for intake-tickets and intake-repair, from
102
+ .lisa/automations/runs/<loop>.jsonl. recent = last run within
103
+ windowHours (2 x stall.noOutputMinutes x stall.maxAttempts, at
104
+ least 24h). Run logs only: the runtime's scheduler is the
105
+ authority on whether a loop is still scheduled.
82
106
 
83
107
  Per cycle
84
108
  queue --tracker github [--me <id>] [--label <name>] [--github-repo <o/r>]
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Read and clear the system clipboard, for `store-token --from-clipboard`.
3
+ *
4
+ * `store-token` prompts on a TTY with echo off, and an agent's Bash has no
5
+ * TTY. Neither does Claude Code's `!` prefix, which is the first thing an
6
+ * operator inside Claude Code tries, so the only working route used to be a
7
+ * separate terminal app (CodySwannGT/lisa#4465). The clipboard is the route an
8
+ * operator reaches for instead: they copy the token from BotFather and the
9
+ * agent stores it without ever seeing it.
10
+ *
11
+ * What is read is a secret. It is returned to the caller and never placed in
12
+ * an error message, a log line or a return value of anything else here.
13
+ * @module session-manager/clipboard
14
+ */
15
+ import { spawnSync } from "node:child_process";
16
+
17
+ /** Upper bound for one clipboard tool call. */
18
+ const TOOL_TIMEOUT_MS = 5000;
19
+
20
+ /**
21
+ * Clipboard tools per platform, tried in order: the first one that runs is
22
+ * used for both reading and clearing.
23
+ */
24
+ const TOOLS = Object.freeze({
25
+ darwin: [{ name: "pbpaste", read: ["pbpaste"], clear: ["pbcopy"] }],
26
+ linux: [
27
+ {
28
+ name: "wl-paste",
29
+ read: ["wl-paste", "--no-newline"],
30
+ clear: ["wl-copy", "--clear"],
31
+ },
32
+ {
33
+ name: "xclip",
34
+ read: ["xclip", "-o", "-selection", "clipboard"],
35
+ clear: ["xclip", "-i", "-selection", "clipboard"],
36
+ },
37
+ {
38
+ name: "xsel",
39
+ read: ["xsel", "--clipboard", "--output"],
40
+ clear: ["xsel", "--clipboard", "--clear"],
41
+ },
42
+ ],
43
+ });
44
+
45
+ /**
46
+ * Run one tool with empty stdin.
47
+ *
48
+ * A clear (`capture: false`) discards the tool's output instead of piping it:
49
+ * `xclip -i` forks a background selection owner, and a child holding an
50
+ * inherited pipe makes `spawnSync` wait out the whole timeout. Any spawn
51
+ * error (a timeout included) is a failure, never a success.
52
+ * @param {string[]} argv - Command and arguments
53
+ * @param {{ capture?: boolean }} [options] - Whether to capture stdout
54
+ * @returns {{ status: number | null, stdout: string, missing: boolean }}
55
+ * Outcome; `missing` when the binary is not on PATH
56
+ */
57
+ function runTool([command, ...args], { capture = true } = {}) {
58
+ const result = spawnSync(command, args, {
59
+ input: "",
60
+ encoding: "utf8",
61
+ timeout: TOOL_TIMEOUT_MS,
62
+ stdio: ["pipe", capture ? "pipe" : "ignore", "ignore"],
63
+ });
64
+ return {
65
+ status: result.error ? null : result.status,
66
+ stdout: result.stdout ?? "",
67
+ missing: result.error?.code === "ENOENT",
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Read the clipboard with the first available tool for `platform`.
73
+ * @param {{ platform?: string, run?: typeof runTool }} [seams] - Platform and
74
+ * the tool runner
75
+ * @returns {{ ok: true, text: string, clear: () => boolean } |
76
+ * { ok: false, reason: "unsupported" | "unavailable" | "failed",
77
+ * message: string }} The text and a function that clears the clipboard
78
+ * (true when it did), or why nothing was read
79
+ */
80
+ export function readClipboard(seams = {}) {
81
+ const platform = seams.platform ?? process.platform;
82
+ const run = seams.run ?? runTool;
83
+ const tools = TOOLS[platform];
84
+ if (!tools) {
85
+ return {
86
+ ok: false,
87
+ reason: "unsupported",
88
+ message: `Reading the clipboard is not supported on ${platform}.`,
89
+ };
90
+ }
91
+ const failed = [];
92
+ for (const tool of tools) {
93
+ const result = run(tool.read);
94
+ if (result.missing) continue;
95
+ // An installed tool can still fail where another works (wl-paste on an
96
+ // X11 session), so a failure moves on to the next tool.
97
+ if (result.status !== 0) {
98
+ failed.push(`${tool.name} exited ${result.status}`);
99
+ continue;
100
+ }
101
+ return {
102
+ ok: true,
103
+ text: result.stdout,
104
+ clear: () => run(tool.clear, { capture: false }).status === 0,
105
+ };
106
+ }
107
+ if (failed.length > 0) {
108
+ return {
109
+ ok: false,
110
+ reason: "failed",
111
+ message: `${failed.join("; ")}; nothing was read.`,
112
+ };
113
+ }
114
+ return {
115
+ ok: false,
116
+ reason: "unavailable",
117
+ message: `No clipboard tool found (tried ${tools.map(t => t.name).join(", ")}).`,
118
+ };
119
+ }
@@ -11,12 +11,12 @@
11
11
  * registry populates defaults into every project, and the manager starts
12
12
  * yolo-mode sessions; it must exist only where an operator wrote it.
13
13
  *
14
- * Pure apart from `detectRepos`, whose filesystem calls are injectable.
14
+ * Pure. Repo detection, which reads the filesystem, lives in repo-detect.mjs
15
+ * and is re-exported here so callers keep one import.
15
16
  * @module session-manager/config
16
17
  */
17
18
 
18
- import { existsSync, readdirSync } from "node:fs";
19
- import { isAbsolute, join, resolve, sep } from "node:path";
19
+ import { isAbsolute, resolve, sep } from "node:path";
20
20
 
21
21
  import {
22
22
  mergeConfigLayers as mergeLayers,
@@ -25,6 +25,7 @@ import {
25
25
  } from "../../lisa-secrets-access/scripts/validate-config.mjs";
26
26
 
27
27
  export { parseLoopInterval, validateSessionManager };
28
+ export { detectRepos, draftHints } from "./repo-detect.mjs";
28
29
 
29
30
  /**
30
31
  * Documented defaults for every optional key.
@@ -122,33 +123,6 @@ export function resolveSessionManagerConfig(globalConfig, localConfig) {
122
123
  };
123
124
  }
124
125
 
125
- /**
126
- * List the Lisa-configured child repos of a multi-repo project.
127
- *
128
- * A child counts only when it carries its own `.lisa.config.json`: workers run
129
- * `/lisa:implement` inside it, which needs that file, and a stray directory
130
- * under `projects/` is not something to route tickets into.
131
- * @param {string} root Project root.
132
- * @param {{
133
- * reposRoot?: string,
134
- * fs?: { existsSync: (p: string) => boolean,
135
- * readdirSync: (p: string, o: { withFileTypes: true }) =>
136
- * Array<{ name: string, isDirectory: () => boolean }> },
137
- * }} [options] Repos root (default `projects`) and filesystem seam.
138
- * @returns {string[]} Child directory names, sorted.
139
- */
140
- export function detectRepos(root, options = {}) {
141
- const fs = options.fs ?? { existsSync, readdirSync };
142
- const base = join(root, options.reposRoot ?? DEFAULT_REPOS_ROOT);
143
- if (!fs.existsSync(base)) return [];
144
- return fs
145
- .readdirSync(base, { withFileTypes: true })
146
- .filter(entry => entry.isDirectory() && SAFE_SEGMENT.test(entry.name))
147
- .filter(entry => fs.existsSync(join(base, entry.name, ".lisa.config.json")))
148
- .map(entry => entry.name)
149
- .sort();
150
- }
151
-
152
126
  /**
153
127
  * Turn a project name into a slug the validator accepts.
154
128
  * @param {string} projectSlug Project name, possibly with dots or capitals.
@@ -177,13 +151,18 @@ function normalizeSlug(projectSlug) {
177
151
  * Defaults are written out rather than left implicit, so an operator sees
178
152
  * every knob in the file they will edit. With no detected repos the block has
179
153
  * no `repos` key, which is what makes a project single-repo. Each detected
180
- * repo's hints start as its own name: valid, and an obvious prompt to add real
181
- * ones.
182
- * @param {{ projectSlug: string, detectedRepos?: string[] }} input Project
183
- * slug and the output of `detectRepos`.
154
+ * repo's hints are its drafted hints when given (`draftHints`), else its own
155
+ * name: valid either way, and setup asks the operator to review them.
156
+ * @param {{ projectSlug: string, detectedRepos?: string[],
157
+ * hints?: Record<string, string[]> }} input Project slug, the output of
158
+ * `detectRepos`, and drafted hints per repo.
184
159
  * @returns {SessionManagerBlock} A block `validateSessionManager` accepts.
185
160
  */
186
- export function buildDefaultBlock({ projectSlug, detectedRepos = [] }) {
161
+ export function buildDefaultBlock({
162
+ projectSlug,
163
+ detectedRepos = [],
164
+ hints = {},
165
+ }) {
187
166
  const slug = normalizeSlug(projectSlug);
188
167
  const repos = [...detectedRepos].sort();
189
168
  return {
@@ -194,7 +173,10 @@ export function buildDefaultBlock({ projectSlug, detectedRepos = [] }) {
194
173
  repos: {
195
174
  root: DEFAULT_REPOS_ROOT,
196
175
  entries: Object.fromEntries(
197
- repos.map(name => [name, { hints: [name] }])
176
+ repos.map(name => [
177
+ name,
178
+ { hints: hints[name]?.length ? [...hints[name]] : [name] },
179
+ ])
198
180
  ),
199
181
  },
200
182
  }
@@ -43,6 +43,7 @@ export {
43
43
  buildStoreTokenCommand,
44
44
  loadPromptSecret,
45
45
  storeToken,
46
+ storeTokenFromClipboard,
46
47
  validateBotToken,
47
48
  writeTokenFile,
48
49
  } from "./token.mjs";
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Which children of a multi-repo project's repos root are real, routable
3
+ * repos, and a first draft of the hints the manager routes on.
4
+ *
5
+ * A host's `projects/` dir collects more than one checkout per repo: linked
6
+ * worktrees agents made for a ticket (`.git` is a FILE pointing back at the
7
+ * main clone), and whole second clones of the same remote (`api-se1234`).
8
+ * Each carries the repo's own `.lisa.config.json`, so counting config files
9
+ * alone proposed 110 routed repos for a project with 4 (CodySwannGT/lisa#4465)
10
+ * and the operator had to prune them by hand. A worker routed into a stale
11
+ * ticket worktree builds on the wrong branch, so the detector keeps one
12
+ * checkout per remote: the one named after the repo when there is one.
13
+ *
14
+ * Every filesystem call goes through an injectable seam.
15
+ * @module session-manager/repo-detect
16
+ */
17
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ /** One path segment that cannot climb out of, or hide inside, its parent. */
21
+ const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
22
+
23
+ /** `url = <value>` inside the `[remote "origin"]` section of a git config. */
24
+ const ORIGIN_SECTION = /^\s*\[remote\s+"origin"\]\s*$/;
25
+ const ANY_SECTION = /^\s*\[/;
26
+ const URL_LINE = /^\s*url\s*=\s*(\S+)\s*$/;
27
+
28
+ /**
29
+ * Well-known dependencies and the hint each one implies. Order is the order
30
+ * hints are listed in. A key ending in `/` matches every package under that
31
+ * scope.
32
+ */
33
+ const DEPENDENCY_HINTS = Object.freeze([
34
+ ["@nestjs/", "NestJS"],
35
+ ["typeorm", "TypeORM"],
36
+ ["prisma", "Prisma"],
37
+ ["@prisma/client", "Prisma"],
38
+ ["@apollo/server", "GraphQL API"],
39
+ ["graphql", "GraphQL"],
40
+ ["express", "Express"],
41
+ ["fastify", "Fastify"],
42
+ ["serverless", "Serverless"],
43
+ ["expo", "Expo"],
44
+ ["react-native", "React Native"],
45
+ ["next", "Next.js"],
46
+ ["react", "React"],
47
+ ["vue", "Vue"],
48
+ ["@angular/core", "Angular"],
49
+ ["@apollo/client", "Apollo client"],
50
+ ["aws-cdk-lib", "AWS CDK"],
51
+ ["@pulumi/pulumi", "Pulumi"],
52
+ ["@langchain/langgraph", "LangGraph"],
53
+ ["@langchain/core", "LangChain"],
54
+ ["@anthropic-ai/sdk", "Claude API"],
55
+ ["@aws-sdk/client-bedrock-runtime", "Bedrock"],
56
+ ["phaser", "Phaser"],
57
+ ["@playwright/test", "Playwright"],
58
+ ]);
59
+
60
+ /** The default filesystem seam. */
61
+ const NODE_FS = { existsSync, readdirSync, readFileSync, statSync };
62
+
63
+ /**
64
+ * Reduce a git remote URL to `host/owner/repo` so the SSH and HTTPS spellings
65
+ * of one remote compare equal. Returns null for anything unparseable.
66
+ * @param {string} url - Remote URL as written in `.git/config`
67
+ * @returns {string | null} Normalized identity, lowercased
68
+ */
69
+ export function normalizeRemote(url) {
70
+ const trimmed = String(url ?? "").trim();
71
+ const scp = /^[^@/]+@([^:/]+):(.+)$/.exec(trimmed);
72
+ const parsed = scp
73
+ ? null
74
+ : /^[a-z+]+:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(trimmed);
75
+ const match = scp ?? parsed;
76
+ if (!match) return null;
77
+ const repoPath = match[2].replace(/\/+$/, "").replace(/\.git$/, "");
78
+ return repoPath ? `${match[1]}/${repoPath}`.toLowerCase() : null;
79
+ }
80
+
81
+ /**
82
+ * The `origin` URL from a clone's `.git/config`, or null.
83
+ * @param {string} dir - Child directory
84
+ * @param {typeof NODE_FS} fs - Filesystem seam
85
+ * @returns {string | null} Raw URL
86
+ */
87
+ function readOrigin(dir, fs) {
88
+ let text;
89
+ try {
90
+ text = fs.readFileSync(join(dir, ".git", "config"), "utf8");
91
+ } catch {
92
+ return null;
93
+ }
94
+ let inOrigin = false;
95
+ for (const line of text.split(/\r?\n/)) {
96
+ if (ANY_SECTION.test(line)) inOrigin = ORIGIN_SECTION.test(line);
97
+ const url = inOrigin ? URL_LINE.exec(line) : null;
98
+ if (url) return url[1];
99
+ }
100
+ return null;
101
+ }
102
+
103
+ /** The `gitdir:` line of a linked worktree points into `.git/worktrees/`. */
104
+ const WORKTREE_GITDIR = /^gitdir:\s*\S*[\\/]worktrees[\\/]/m;
105
+
106
+ /**
107
+ * Whether `<dir>` is a linked worktree: its `.git` is a file whose `gitdir:`
108
+ * points into another clone's `worktrees/`. A `.git` file that points
109
+ * anywhere else (a submodule, `--separate-git-dir`) may be the only checkout
110
+ * of its repo, so it is kept.
111
+ * @param {string} dir - Child directory
112
+ * @param {typeof NODE_FS} fs - Filesystem seam
113
+ * @returns {boolean} True for a linked worktree
114
+ */
115
+ function isLinkedWorktree(dir, fs) {
116
+ const marker = join(dir, ".git");
117
+ try {
118
+ if (!fs.statSync(marker).isFile()) return false;
119
+ return WORKTREE_GITDIR.test(fs.readFileSync(marker, "utf8"));
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Keep one child per remote: the one whose name is the remote's repo name,
127
+ * else the lexically first. Children with no readable origin are each kept.
128
+ * @param {Array<{ name: string, remote: string | null }>} children - Sorted
129
+ * @returns {string[]} Kept names, sorted
130
+ */
131
+ function onePerRemote(children) {
132
+ const groups = new Map();
133
+ const kept = [];
134
+ for (const child of children) {
135
+ if (child.remote === null) kept.push(child.name);
136
+ else groups.set(child.remote, [...(groups.get(child.remote) ?? []), child]);
137
+ }
138
+ for (const [remote, members] of groups) {
139
+ const repoName = remote.split("/").at(-1);
140
+ const named = members.find(
141
+ member => member.name.toLowerCase() === repoName
142
+ );
143
+ kept.push((named ?? members[0]).name);
144
+ }
145
+ return kept.sort();
146
+ }
147
+
148
+ /**
149
+ * List the routable child repos of a multi-repo project.
150
+ *
151
+ * A child counts only when it carries its own `.lisa.config.json` (workers
152
+ * run `/lisa:implement` inside it, which needs that file), is not a linked
153
+ * worktree, and is the chosen checkout of its remote.
154
+ * @param {string} root - Project root
155
+ * @param {{ reposRoot?: string, fs?: Partial<typeof NODE_FS> }} [options] -
156
+ * Repos root (default `projects`) and filesystem seam
157
+ * @returns {string[]} Child directory names, sorted
158
+ */
159
+ export function detectRepos(root, options = {}) {
160
+ const fs = { ...NODE_FS, ...options.fs };
161
+ const base = join(root, options.reposRoot ?? "projects");
162
+ if (!fs.existsSync(base)) return [];
163
+ const children = fs
164
+ .readdirSync(base, { withFileTypes: true })
165
+ .filter(entry => entry.isDirectory() && SAFE_SEGMENT.test(entry.name))
166
+ .map(entry => ({ name: entry.name, dir: join(base, entry.name) }))
167
+ .filter(child => fs.existsSync(join(child.dir, ".lisa.config.json")))
168
+ .filter(child => !isLinkedWorktree(child.dir, fs))
169
+ .map(child => ({
170
+ name: child.name,
171
+ remote: normalizeRemote(readOrigin(child.dir, fs) ?? ""),
172
+ }))
173
+ .sort((a, b) => (a.name < b.name ? -1 : 1));
174
+ return onePerRemote(children);
175
+ }
176
+
177
+ /**
178
+ * Draft routing hints for one child: its name, then a hint per well-known
179
+ * dependency in its `package.json`. Only a draft; setup asks the operator to
180
+ * review it. A child with no `package.json`, or none of the known
181
+ * dependencies, keeps just its name.
182
+ * @param {string} dir - Child directory
183
+ * @param {string} name - Child name (always the first hint)
184
+ * @param {{ fs?: Partial<typeof NODE_FS> }} [options] - Filesystem seam
185
+ * @returns {string[]} Hints, de-duplicated, name first
186
+ */
187
+ export function draftHints(dir, name, options = {}) {
188
+ const fs = { ...NODE_FS, ...options.fs };
189
+ let manifest;
190
+ try {
191
+ manifest = JSON.parse(fs.readFileSync(join(dir, "package.json"), "utf8"));
192
+ } catch {
193
+ return [name];
194
+ }
195
+ const deps = Object.keys({
196
+ ...manifest?.dependencies,
197
+ ...manifest?.devDependencies,
198
+ });
199
+ const hints = DEPENDENCY_HINTS.filter(([key]) =>
200
+ key.endsWith("/")
201
+ ? deps.some(dep => dep.startsWith(key))
202
+ : deps.includes(key)
203
+ ).map(([, hint]) => hint);
204
+ return [...new Set([name, ...hints])];
205
+ }