@atollhq/skill-claude 0.4.23 → 0.4.24

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 (3) hide show
  1. package/README.md +19 -5
  2. package/bin/install.mjs +166 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,17 +6,31 @@ Gives your Claude Code agent the ability to manage tasks, goals, KPIs, initiativ
6
6
 
7
7
  ## Install
8
8
 
9
+ Install or refresh the local skill files without configuring credentials:
10
+
11
+ ```bash
12
+ npx --yes @atollhq/skill-claude@latest
13
+ ```
14
+
15
+ Bare invocation and `--install-only` only update `~/.claude/skills/atoll/`. They do not inspect or change Atoll credentials, profiles, shell files, or Claude settings, even when `ATOLL_*` variables are present.
16
+
17
+ Configure a named profile explicitly:
18
+
19
+ ```bash
20
+ npx --yes @atollhq/skill-claude@latest --profile agent-a --key sk_atoll_... --org org-uuid --project project-id --team team-id
21
+ ```
22
+
23
+ For intentional environment configuration, use `--configure`:
24
+
9
25
  ```bash
10
- npx @atollhq/skill-claude@latest --profile agent-a --key sk_atoll_... --org your-org-id --project project-id --team team-id
11
- # or
12
- ATOLL_API_KEY=sk_atoll_... ATOLL_ORG_ID=your-org-id npx @atollhq/skill-claude@latest
26
+ ATOLL_API_KEY=sk_atoll_... ATOLL_ORG_ID=org-uuid npx --yes @atollhq/skill-claude@latest --configure
13
27
  ```
14
28
 
15
- Optional defaults: `--project`, `--team`, and `--base-url` are stored with the selected mode. Use `--no-project`, `--no-team`, or `--no-base-url` to clear previously saved defaults. Pass `--profile` to store credentials and defaults only in that named Atoll CLI profile. The installer does not write a global `ATOLL_PROFILE` or `ATOLL_*` credential settings in profile mode, so direct CLI commands should use `atoll --profile agent-a ...`. Omit `--profile` to use env-var mode, which writes `ATOLL_ENV_MODE=1` with the credential settings.
29
+ Configuration mode supports `--project`, `--team`, and `--base-url`. Use `--no-project`, `--no-team`, or `--no-base-url` to clear saved defaults. Pass `--profile` to store credentials and defaults only in that named Atoll CLI profile. The installer does not write a global `ATOLL_PROFILE` or `ATOLL_*` credential setting in profile mode, so direct CLI commands should use `atoll --profile agent-a ...`.
16
30
 
17
31
  Get an agent API key from **Agents** in the Atoll app. Integration keys are managed from **Settings > Integrations**.
18
32
 
19
- This does three things:
33
+ Every invocation copies the skill. Configuration mode additionally:
20
34
 
21
35
  1. Copies the skill into `~/.claude/skills/atoll/`
22
36
  2. Removes stale Atoll credential/profile env from `~/.claude/settings.json` when profile mode is used
package/bin/install.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  readFileSync,
12
12
  renameSync,
13
13
  rmSync,
14
+ statSync,
14
15
  writeFileSync,
15
16
  } from 'node:fs'
16
17
  import { basename, dirname, join } from 'node:path'
@@ -20,30 +21,60 @@ import { fileURLToPath } from 'node:url'
20
21
 
21
22
  const __dirname = dirname(fileURLToPath(import.meta.url))
22
23
  const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'))
24
+ const configurationFlags = new Set([
25
+ '--configure',
26
+ '--profile',
27
+ '--key',
28
+ '--org',
29
+ '--project',
30
+ '--no-project',
31
+ '--team',
32
+ '--no-team',
33
+ '--base-url',
34
+ '--no-base-url',
35
+ ])
36
+ const valueFlags = {
37
+ '--profile': 'profile',
38
+ '--key': 'key',
39
+ '--org': 'org',
40
+ '--project': 'project',
41
+ '--team': 'team',
42
+ '--base-url': 'baseUrl',
43
+ }
44
+
45
+ function hasConfigurationIntent(argv) {
46
+ return argv.slice(2).some((arg) => configurationFlags.has(arg.split('=', 1)[0]))
47
+ }
23
48
 
24
49
  function parseArgs(argv) {
25
50
  const args = {}
26
51
  for (let i = 2; i < argv.length; i++) {
27
- if (argv[i] === '--profile' && argv[i + 1]) {
28
- args.profile = argv[++i]
29
- } else if (argv[i] === '--key' && argv[i + 1]) {
30
- args.key = argv[++i]
31
- } else if (argv[i] === '--org' && argv[i + 1]) {
32
- args.org = argv[++i]
33
- } else if (argv[i] === '--project' && argv[i + 1]) {
34
- args.project = argv[++i]
35
- } else if (argv[i] === '--no-project') {
52
+ const argument = argv[i]
53
+ const separator = argument.indexOf('=')
54
+ const flag = separator === -1 ? argument : argument.slice(0, separator)
55
+ const inlineValue = separator === -1 ? undefined : argument.slice(separator + 1)
56
+ const nextValue = inlineValue ?? argv[i + 1]
57
+ const valueKey = valueFlags[flag]
58
+ if (valueKey) {
59
+ if (!nextValue || nextValue.startsWith('-')) {
60
+ throw new Error(`${flag} requires a non-empty value`)
61
+ }
62
+ args[valueKey] = nextValue
63
+ if (inlineValue === undefined) i++
64
+ } else if (flag === '--no-project') {
36
65
  args.clearProject = true
37
- } else if (argv[i] === '--team' && argv[i + 1]) {
38
- args.team = argv[++i]
39
- } else if (argv[i] === '--no-team') {
66
+ } else if (flag === '--no-team') {
40
67
  args.clearTeam = true
41
- } else if (argv[i] === '--base-url' && argv[i + 1]) {
42
- args.baseUrl = argv[++i]
43
- } else if (argv[i] === '--no-base-url') {
68
+ } else if (flag === '--no-base-url') {
44
69
  args.clearBaseUrl = true
45
- } else if (argv[i] === '--help' || argv[i] === '-h') {
70
+ } else if (flag === '--install-only') {
71
+ args.installOnly = true
72
+ } else if (flag === '--configure') {
73
+ args.configure = true
74
+ } else if (flag === '--help' || flag === '-h') {
46
75
  args.help = true
76
+ } else {
77
+ throw new Error(`Unknown option: ${flag}`)
47
78
  }
48
79
  }
49
80
  return args
@@ -51,50 +82,67 @@ function parseArgs(argv) {
51
82
 
52
83
  function printUsage() {
53
84
  console.log(`
54
- Usage: npx @atollhq/skill-claude@latest [--profile <name>] --key <api-key> --org <org-id> [--project <id>] [--team <id-or-slug>] [--base-url <url>]
55
- or: ATOLL_API_KEY=<api-key> ATOLL_ORG_ID=<org-id> npx @atollhq/skill-claude@latest
85
+ Usage: npx @atollhq/skill-claude@latest --install-only
86
+ or: npx @atollhq/skill-claude@latest --configure [--profile <name>] --key <api-key> --org <org-id> [--project <id>] [--team <id-or-slug>] [--base-url <url>]
87
+ or: ATOLL_API_KEY=<api-key> ATOLL_ORG_ID=<org-id> npx @atollhq/skill-claude@latest --configure
56
88
 
57
89
  Options:
58
- --profile Atoll CLI profile name to create/update.
59
- --key Atoll API key (sk_atoll_...). Defaults to ATOLL_API_KEY.
60
- --org Organization ID. Defaults to ATOLL_ORG_ID.
61
- --project Default project ID. Defaults to ATOLL_PROJECT.
90
+ --profile Atoll CLI profile name to create/update. Enables configuration mode.
91
+ --key Atoll API key (sk_atoll_...). Used in configuration mode; defaults to ATOLL_API_KEY.
92
+ --org Organization ID. Used in configuration mode; defaults to ATOLL_ORG_ID.
93
+ --project Default project ID. Used in configuration mode; defaults to ATOLL_PROJECT.
62
94
  --no-project Clear any saved default project.
63
- --team Default team ID or slug. Defaults to ATOLL_TEAM.
95
+ --team Default team ID or slug. Used in configuration mode; defaults to ATOLL_TEAM.
64
96
  --no-team Clear any saved default team.
65
- --base-url Atoll base URL. Defaults to ATOLL_BASE_URL.
97
+ --base-url Atoll base URL. Used in configuration mode; defaults to ATOLL_BASE_URL.
66
98
  --no-base-url Clear any saved custom base URL.
99
+ --install-only
100
+ Install or refresh local skill files without reading or changing credentials.
101
+ --configure Explicitly enable profile or environment credential configuration.
67
102
  --help Show this help message
68
103
 
69
104
  This installs the Atoll skill for Claude Code, giving your agent
70
- the ability to manage tasks, goals, KPIs, and initiatives.
105
+ the ability to manage tasks, goals, KPIs, and initiatives. Authentication
106
+ and profile configuration remain unchanged unless --configure or another
107
+ configuration flag is provided.
71
108
  `)
72
109
  }
73
110
 
74
111
  const args = parseArgs(process.argv)
75
- args.key ??= process.env.ATOLL_API_KEY
76
- args.org ??= process.env.ATOLL_ORG_ID
77
- if (args.clearProject) delete args.project
78
- else args.project ??= process.env.ATOLL_PROJECT
79
- if (args.clearTeam) delete args.team
80
- else args.team ??= process.env.ATOLL_TEAM
81
- if (args.clearBaseUrl) delete args.baseUrl
82
- else args.baseUrl ??= process.env.ATOLL_BASE_URL
112
+ const configurationIntent = hasConfigurationIntent(process.argv)
113
+
114
+ if (args.installOnly && configurationIntent) {
115
+ console.error('Error: --install-only cannot be combined with credential or profile configuration flags')
116
+ printUsage()
117
+ process.exit(1)
118
+ }
83
119
 
84
120
  if (args.help) {
85
121
  printUsage()
86
122
  process.exit(0)
87
123
  }
88
124
 
125
+ const configureMode = configurationIntent
126
+ if (configureMode) {
127
+ args.key ??= process.env.ATOLL_API_KEY
128
+ args.org ??= process.env.ATOLL_ORG_ID
129
+ if (args.clearProject) delete args.project
130
+ else args.project ??= process.env.ATOLL_PROJECT
131
+ if (args.clearTeam) delete args.team
132
+ else args.team ??= process.env.ATOLL_TEAM
133
+ if (args.clearBaseUrl) delete args.baseUrl
134
+ else args.baseUrl ??= process.env.ATOLL_BASE_URL
135
+ }
136
+
89
137
  console.log(`Running @atollhq/skill-claude installer v${packageJson.version}`)
90
138
 
91
- if (!args.key || !args.org) {
139
+ if (configureMode && (!args.key || !args.org)) {
92
140
  console.error('Error: provide --key and --org, or set ATOLL_API_KEY and ATOLL_ORG_ID\n')
93
141
  printUsage()
94
142
  process.exit(1)
95
143
  }
96
144
 
97
- if (!args.key.startsWith('sk_atoll_')) {
145
+ if (configureMode && !args.key.startsWith('sk_atoll_')) {
98
146
  console.error('Error: API key must start with sk_atoll_')
99
147
  process.exit(1)
100
148
  }
@@ -134,6 +182,52 @@ function ensurePrivateDirectory(path) {
134
182
  chmodSync(path, 0o700)
135
183
  }
136
184
 
185
+ function prepareSkillDirectory(path, source) {
186
+ const existing = lstatIfExists(path)
187
+ if (existing) {
188
+ if (existing.isSymbolicLink()) {
189
+ throw new Error(`Refusing symbolic link for skill directory: ${path}`)
190
+ }
191
+ if (!existing.isDirectory()) {
192
+ throw new Error(`Refusing non-directory skill path: ${path}`)
193
+ }
194
+ }
195
+
196
+ const parent = dirname(path)
197
+ mkdirSync(parent, { recursive: true })
198
+ const tempPath = join(parent, `.${basename(path)}.atoll-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`)
199
+ const backupPath = `${tempPath}.backup`
200
+ let movedExisting = false
201
+ let installed = false
202
+ try {
203
+ cpSync(source, tempPath, { recursive: true })
204
+
205
+ const current = lstatIfExists(path)
206
+ if (current) {
207
+ if (current.isSymbolicLink()) {
208
+ throw new Error(`Refusing symbolic link for skill directory: ${path}`)
209
+ }
210
+ if (!current.isDirectory()) {
211
+ throw new Error(`Refusing non-directory skill path: ${path}`)
212
+ }
213
+ renameSync(path, backupPath)
214
+ movedExisting = true
215
+ }
216
+
217
+ renameSync(tempPath, path)
218
+ installed = true
219
+ if (movedExisting) rmSync(backupPath, { recursive: true, force: true })
220
+ } catch (error) {
221
+ if (!installed) {
222
+ if (lstatIfExists(tempPath)) rmSync(tempPath, { recursive: true, force: true })
223
+ if (movedExisting && !lstatIfExists(path) && lstatIfExists(backupPath)) {
224
+ renameSync(backupPath, path)
225
+ }
226
+ }
227
+ throw error
228
+ }
229
+ }
230
+
137
231
  function readJson(path) {
138
232
  assertSafeFile(path)
139
233
  if (!existsSync(path)) return {}
@@ -163,10 +257,22 @@ function writePrivateFile(path, content) {
163
257
  }
164
258
  }
165
259
 
166
- function acquireInstallerLock() {
167
- const atollDir = join(homedir(), '.atoll')
168
- const target = join(atollDir, 'installer.lock-target')
169
- ensurePrivateDirectory(atollDir)
260
+ const defaultInstallerLockTarget = join(homedir(), '.atoll', 'installer.lock-target')
261
+ const sharedInstallerLockTarget = join(homedir(), '.atoll-skill-installer.lock-target')
262
+
263
+ function ensureLockDirectory(path) {
264
+ const entry = lstatIfExists(path)
265
+ if (entry) {
266
+ const stat = entry.isSymbolicLink() ? statSync(path) : entry
267
+ if (!stat.isDirectory()) throw new Error(`Refusing non-directory installer lock path: ${path}`)
268
+ } else {
269
+ mkdirSync(path, { recursive: true, mode: 0o700 })
270
+ }
271
+ }
272
+
273
+ function acquireInstallerLock(target = defaultInstallerLockTarget) {
274
+ if (target === defaultInstallerLockTarget) ensurePrivateDirectory(dirname(target))
275
+ else ensureLockDirectory(dirname(target))
170
276
  try {
171
277
  writeFileSync(target, '', { flag: 'wx', mode: 0o600 })
172
278
  } catch (error) {
@@ -222,7 +328,19 @@ function acquireInstallerLock() {
222
328
  }
223
329
  }
224
330
 
225
- const releaseInstallerLock = acquireInstallerLock()
331
+ const releaseSharedInstallerLock = acquireInstallerLock(sharedInstallerLockTarget)
332
+ let releaseConfigInstallerLock = null
333
+ try {
334
+ if (configureMode) releaseConfigInstallerLock = acquireInstallerLock()
335
+ } catch (error) {
336
+ releaseSharedInstallerLock()
337
+ throw error
338
+ }
339
+
340
+ function releaseInstallerLock() {
341
+ releaseConfigInstallerLock?.()
342
+ releaseSharedInstallerLock()
343
+ }
226
344
 
227
345
  function writeAtollProfile() {
228
346
  if (!args.profile) return
@@ -255,14 +373,20 @@ const skillDest = join(homedir(), '.claude', 'skills', 'atoll')
255
373
  const legacySkillDest = join(homedir(), '.claude', 'skills', 'atoll-api')
256
374
  ensurePrivateDirectory(join(homedir(), '.claude'))
257
375
 
258
- mkdirSync(join(skillDest, 'references'), { recursive: true })
259
- cpSync(skillSrc, skillDest, { recursive: true })
376
+ prepareSkillDirectory(skillDest, skillSrc)
260
377
  console.log(`Installed skill to ${skillDest}`)
261
378
  if (existsSync(legacySkillDest)) {
262
379
  rmSync(legacySkillDest, { recursive: true, force: true })
263
380
  console.log(`Removed legacy Atoll skill at ${legacySkillDest}`)
264
381
  }
265
382
 
383
+ if (!configureMode) {
384
+ console.log('Authentication/profile configuration left unchanged.')
385
+ console.log('\nDone! Start Claude Code and the atoll skill will be available.')
386
+ releaseInstallerLock()
387
+ process.exit(0)
388
+ }
389
+
266
390
  // 2. Configure ~/.claude/settings.json. Profile mode stores credentials only
267
391
  // in the Atoll CLI profile and removes stale credential/profile env from
268
392
  // earlier installs so a new profile install does not become globally active.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atollhq/skill-claude",
3
- "version": "0.4.23",
3
+ "version": "0.4.24",
4
4
  "description": "Install the Atoll project management skill for Claude Code",
5
5
  "bin": {
6
6
  "skill-claude": "bin/install.mjs"