cdragon 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/bin/cdragon.js +34 -10
- package/package.json +1 -1
- package/skills/gws-sheets/SKILL.md +54 -0
- package/skills/gws-sheets-append/SKILL.md +55 -0
- package/skills/gws-sheets-read/SKILL.md +48 -0
- package/skills/herdr-cli/scripts/herdr-agent-run-and-wait +0 -0
- package/skills/herdr-cli/scripts/herdr-agent-wait-complete +0 -0
- package/src/link.js +18 -1
- package/src/prompt.js +20 -9
- package/src/skills.js +15 -0
package/README.md
CHANGED
|
@@ -91,6 +91,27 @@ cdragon --global --both tdd grill-me # ~/.claude, ~/.agents 양쪽에 일부
|
|
|
91
91
|
|
|
92
92
|
이미 존재하는 **실제 디렉터리**는 덮어쓰지 않고 건너뜁니다. 같은 대상을 가리키는 심링크는 그대로 두고, 다른 곳을 가리키면 다시 연결합니다.
|
|
93
93
|
|
|
94
|
+
### 개발
|
|
95
|
+
|
|
96
|
+
레포 루트에서 한 번 `npm link` 해두면, 이후 코드 수정은 재링크 없이 바로 반영됩니다.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm link # 최초 1회
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 배포
|
|
103
|
+
|
|
104
|
+
스킬을 추가했거나 CLI를 수정했다면 버전을 올려 다시 배포합니다. **버전을 올리지 않으면 `npm publish`가 거부됩니다** (같은 버전 재배포 불가).
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm version patch # 0.1.0 → 0.1.1 (버그픽스) — 커밋+태그 자동 생성
|
|
108
|
+
# npm version minor # 0.1.0 → 0.2.0 (기능 추가)
|
|
109
|
+
git push --follow-tags # 커밋과 태그를 함께 푸시
|
|
110
|
+
npm publish --access public # 게시 (보안키/OTP 인증)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`npm version`이 `package.json` 버전 변경·커밋·git 태그를 한 번에 처리합니다. 사용자는 `npm i -g cdragon@latest`로 갱신합니다.
|
|
114
|
+
|
|
94
115
|
## Getting Started
|
|
95
116
|
|
|
96
117
|
```bash
|
package/bin/cdragon.js
CHANGED
|
@@ -8,10 +8,20 @@ const pkg = require('../package.json')
|
|
|
8
8
|
const c = require('../src/colors')
|
|
9
9
|
const prompt = require('../src/prompt')
|
|
10
10
|
const { SKILLS_DIR, discoverSkills } = require('../src/skills')
|
|
11
|
-
const { linkSkills } = require('../src/link')
|
|
11
|
+
const { linkSkills, isInstalledIn } = require('../src/link')
|
|
12
12
|
|
|
13
13
|
const truncate = (s, n) => (s.length > n ? s.slice(0, n - 1) + '…' : s)
|
|
14
14
|
|
|
15
|
+
// Split skills into ordered, non-empty groups by where they came from.
|
|
16
|
+
function groupBySource(skills) {
|
|
17
|
+
return [
|
|
18
|
+
{ key: 'mine', label: 'My skills' },
|
|
19
|
+
{ key: 'installed', label: 'External' },
|
|
20
|
+
]
|
|
21
|
+
.map((g) => ({ ...g, items: skills.filter((s) => s.source === g.key) }))
|
|
22
|
+
.filter((g) => g.items.length)
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
function parseArgs(args) {
|
|
16
26
|
const opts = { scope: null, folders: [], all: false, skills: [], yes: false }
|
|
17
27
|
|
|
@@ -66,9 +76,12 @@ function listSkills() {
|
|
|
66
76
|
return
|
|
67
77
|
}
|
|
68
78
|
const width = Math.max(...skills.map((s) => s.name.length))
|
|
69
|
-
console.log(`\n${c.bold(`Skills (${skills.length})`)} ${c.dim(SKILLS_DIR)}
|
|
70
|
-
for (const
|
|
71
|
-
console.log(
|
|
79
|
+
console.log(`\n${c.bold(`Skills (${skills.length})`)} ${c.dim(SKILLS_DIR)}`)
|
|
80
|
+
for (const group of groupBySource(skills)) {
|
|
81
|
+
console.log(`\n ${c.bold(group.label)} ${c.dim(`(${group.items.length})`)}`)
|
|
82
|
+
for (const s of group.items) {
|
|
83
|
+
console.log(` ${c.cyan(s.name.padEnd(width))} ${c.dim(truncate(s.description, 74))}`)
|
|
84
|
+
}
|
|
72
85
|
}
|
|
73
86
|
console.log('')
|
|
74
87
|
}
|
|
@@ -96,6 +109,8 @@ async function linkCommand(opts) {
|
|
|
96
109
|
}
|
|
97
110
|
if (!folders.length) throw new Error('No target folder selected.')
|
|
98
111
|
|
|
112
|
+
const base = scope === 'global' ? os.homedir() : process.cwd()
|
|
113
|
+
|
|
99
114
|
// 3. Skills: all, named, or interactively picked.
|
|
100
115
|
let chosen
|
|
101
116
|
if (opts.all) {
|
|
@@ -107,16 +122,25 @@ async function linkCommand(opts) {
|
|
|
107
122
|
return found
|
|
108
123
|
})
|
|
109
124
|
} else {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
const choices = []
|
|
126
|
+
for (const group of groupBySource(skills)) {
|
|
127
|
+
choices.push({ header: true, label: `${group.label} (${group.items.length})` })
|
|
128
|
+
for (const s of group.items) {
|
|
129
|
+
// Pre-check skills already linked into the chosen target.
|
|
130
|
+
const installed = isInstalledIn(s, base, folders)
|
|
131
|
+
const tag = installed ? c.dim(' (linked)') : ''
|
|
132
|
+
choices.push({
|
|
133
|
+
value: s.name,
|
|
134
|
+
checked: installed,
|
|
135
|
+
label: `${s.name} ${c.dim(truncate(s.description, 56))}${tag}`,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const picked = await prompt.multiselect('Which skills to link?', choices)
|
|
114
140
|
chosen = picked.map((name) => skills.find((s) => s.name === name))
|
|
115
141
|
}
|
|
116
142
|
if (!chosen.length) throw new Error('No skills selected.')
|
|
117
143
|
|
|
118
|
-
const base = scope === 'global' ? os.homedir() : process.cwd()
|
|
119
|
-
|
|
120
144
|
// Summary + confirm.
|
|
121
145
|
console.log('')
|
|
122
146
|
console.log(` ${c.bold('scope')} ${scope} ${c.dim(`(${base})`)}`)
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gws-sheets
|
|
3
|
+
description: "Google Sheets: Read and write spreadsheets."
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.22.5
|
|
6
|
+
openclaw:
|
|
7
|
+
category: "productivity"
|
|
8
|
+
requires:
|
|
9
|
+
bins:
|
|
10
|
+
- gws
|
|
11
|
+
cliHelp: "gws sheets --help"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# sheets (v4)
|
|
15
|
+
|
|
16
|
+
> **PREREQUISITE:** Read `../gws-shared/SKILL.md` for auth, global flags, and security rules. If missing, run `gws generate-skills` to create it.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
gws sheets <resource> <method> [flags]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Helper Commands
|
|
23
|
+
|
|
24
|
+
| Command | Description |
|
|
25
|
+
|---------|-------------|
|
|
26
|
+
| [`+append`](../gws-sheets-append/SKILL.md) | Append a row to a spreadsheet |
|
|
27
|
+
| [`+read`](../gws-sheets-read/SKILL.md) | Read values from a spreadsheet |
|
|
28
|
+
|
|
29
|
+
## API Resources
|
|
30
|
+
|
|
31
|
+
### spreadsheets
|
|
32
|
+
|
|
33
|
+
- `batchUpdate` — Applies one or more updates to the spreadsheet. Each request is validated before being applied. If any request is not valid then the entire request will fail and nothing will be applied. Some requests have replies to give you some information about how they are applied. The replies will mirror the requests. For example, if you applied 4 updates and the 3rd one had a reply, then the response will have 2 empty replies, the actual reply, and another empty reply, in that order.
|
|
34
|
+
- `create` — Creates a spreadsheet, returning the newly created spreadsheet.
|
|
35
|
+
- `get` — Returns the spreadsheet at the given ID. The caller must specify the spreadsheet ID. By default, data within grids is not returned. You can include grid data in one of 2 ways: * Specify a [field mask](https://developers.google.com/workspace/sheets/api/guides/field-masks) listing your desired fields using the `fields` URL parameter in HTTP * Set the includeGridData URL parameter to true.
|
|
36
|
+
- `getByDataFilter` — Returns the spreadsheet at the given ID. The caller must specify the spreadsheet ID. For more information, see [Read, write, and search metadata](https://developers.google.com/workspace/sheets/api/guides/metadata). This method differs from GetSpreadsheet in that it allows selecting which subsets of spreadsheet data to return by specifying a dataFilters parameter. Multiple DataFilters can be specified.
|
|
37
|
+
- `developerMetadata` — Operations on the 'developerMetadata' resource
|
|
38
|
+
- `sheets` — Operations on the 'sheets' resource
|
|
39
|
+
- `values` — Operations on the 'values' resource
|
|
40
|
+
|
|
41
|
+
## Discovering Commands
|
|
42
|
+
|
|
43
|
+
Before calling any API method, inspect it:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Browse resources and methods
|
|
47
|
+
gws sheets --help
|
|
48
|
+
|
|
49
|
+
# Inspect a method's required params, types, and defaults
|
|
50
|
+
gws schema sheets.<resource>.<method>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use `gws schema` output to build your `--params` and `--json` flags.
|
|
54
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gws-sheets-append
|
|
3
|
+
description: "Google Sheets: Append a row to a spreadsheet."
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.22.5
|
|
6
|
+
openclaw:
|
|
7
|
+
category: "productivity"
|
|
8
|
+
requires:
|
|
9
|
+
bins:
|
|
10
|
+
- gws
|
|
11
|
+
cliHelp: "gws sheets +append --help"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# sheets +append
|
|
15
|
+
|
|
16
|
+
> **PREREQUISITE:** Read `../gws-shared/SKILL.md` for auth, global flags, and security rules. If missing, run `gws generate-skills` to create it.
|
|
17
|
+
|
|
18
|
+
Append a row to a spreadsheet
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gws sheets +append --spreadsheet <ID>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Flags
|
|
27
|
+
|
|
28
|
+
| Flag | Required | Default | Description |
|
|
29
|
+
|------|----------|---------|-------------|
|
|
30
|
+
| `--spreadsheet` | ✓ | — | Spreadsheet ID |
|
|
31
|
+
| `--values` | — | — | Comma-separated values (simple strings) |
|
|
32
|
+
| `--json-values` | — | — | JSON array of rows, e.g. '[["a","b"],["c","d"]]' |
|
|
33
|
+
| `--range` | — | `A1` | Target range in A1 notation (e.g. 'Sheet2!A1') to select a specific tab |
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gws sheets +append --spreadsheet ID --values 'Alice,100,true'
|
|
39
|
+
gws sheets +append --spreadsheet ID --json-values '[["a","b"],["c","d"]]'
|
|
40
|
+
gws sheets +append --spreadsheet ID --range "Sheet2!A1" --values 'Alice,100'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Tips
|
|
44
|
+
|
|
45
|
+
- Use --values for simple single-row appends.
|
|
46
|
+
- Use --json-values for bulk multi-row inserts.
|
|
47
|
+
- Use --range to append to a specific sheet tab (default: A1, i.e. first sheet).
|
|
48
|
+
|
|
49
|
+
> [!CAUTION]
|
|
50
|
+
> This is a **write** command — confirm with the user before executing.
|
|
51
|
+
|
|
52
|
+
## See Also
|
|
53
|
+
|
|
54
|
+
- [gws-shared](../gws-shared/SKILL.md) — Global flags and auth
|
|
55
|
+
- [gws-sheets](../gws-sheets/SKILL.md) — All read and write spreadsheets commands
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gws-sheets-read
|
|
3
|
+
description: "Google Sheets: Read values from a spreadsheet."
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.22.5
|
|
6
|
+
openclaw:
|
|
7
|
+
category: "productivity"
|
|
8
|
+
requires:
|
|
9
|
+
bins:
|
|
10
|
+
- gws
|
|
11
|
+
cliHelp: "gws sheets +read --help"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# sheets +read
|
|
15
|
+
|
|
16
|
+
> **PREREQUISITE:** Read `../gws-shared/SKILL.md` for auth, global flags, and security rules. If missing, run `gws generate-skills` to create it.
|
|
17
|
+
|
|
18
|
+
Read values from a spreadsheet
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gws sheets +read --spreadsheet <ID> --range <RANGE>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Flags
|
|
27
|
+
|
|
28
|
+
| Flag | Required | Default | Description |
|
|
29
|
+
|------|----------|---------|-------------|
|
|
30
|
+
| `--spreadsheet` | ✓ | — | Spreadsheet ID |
|
|
31
|
+
| `--range` | ✓ | — | Range to read (e.g. 'Sheet1!A1:B2') |
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gws sheets +read --spreadsheet ID --range "Sheet1!A1:D10"
|
|
37
|
+
gws sheets +read --spreadsheet ID --range Sheet1
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Tips
|
|
41
|
+
|
|
42
|
+
- Read-only — never modifies the spreadsheet.
|
|
43
|
+
- For advanced options, use the raw values.get API.
|
|
44
|
+
|
|
45
|
+
## See Also
|
|
46
|
+
|
|
47
|
+
- [gws-shared](../gws-shared/SKILL.md) — Global flags and auth
|
|
48
|
+
- [gws-sheets](../gws-sheets/SKILL.md) — All read and write spreadsheets commands
|
|
File without changes
|
|
File without changes
|
package/src/link.js
CHANGED
|
@@ -29,6 +29,23 @@ function linkSkill(sourceDir, linkPath) {
|
|
|
29
29
|
return 'exists'
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Is `sourceDir` already symlinked at linkPath? (true only for a symlink that
|
|
33
|
+
// resolves to exactly this source — not a real dir, not a stale link.)
|
|
34
|
+
function isLinked(sourceDir, linkPath) {
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.lstatSync(linkPath).isSymbolicLink()) return false
|
|
37
|
+
return path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath)) === sourceDir
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Is the skill already linked into <base>/<folder>/skills for every folder?
|
|
44
|
+
function isInstalledIn(skill, base, folders) {
|
|
45
|
+
const name = path.basename(skill.dir)
|
|
46
|
+
return folders.every((folder) => isLinked(skill.dir, path.join(base, folder, 'skills', name)))
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
// Link a set of skills into <base>/<folder>/skills, creating the dir if needed.
|
|
33
50
|
function linkSkills(skills, base, folder) {
|
|
34
51
|
const root = path.join(base, folder, 'skills')
|
|
@@ -44,4 +61,4 @@ function linkSkills(skills, base, folder) {
|
|
|
44
61
|
})
|
|
45
62
|
}
|
|
46
63
|
|
|
47
|
-
module.exports = { linkSkill, linkSkills }
|
|
64
|
+
module.exports = { linkSkill, linkSkills, isInstalledIn }
|
package/src/prompt.js
CHANGED
|
@@ -68,23 +68,33 @@ function select(message, choices) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// Multi-choice checkbox list. Resolves an array of selected `value`s.
|
|
71
|
+
// A choice with `{ header: true }` renders as a non-selectable group label
|
|
72
|
+
// and is skipped during navigation.
|
|
71
73
|
function multiselect(message, choices) {
|
|
72
74
|
requireTTY()
|
|
73
75
|
return new Promise((resolve) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const selectable = choices.map((c, i) => (c.header ? -1 : i)).filter((i) => i >= 0)
|
|
77
|
+
// Pre-check any choice flagged `checked` (e.g. already installed).
|
|
78
|
+
const selected = new Set(selectable.filter((i) => choices[i].checked))
|
|
76
79
|
const totalLines = choices.length + 1
|
|
80
|
+
let pos = 0 // index into `selectable`
|
|
77
81
|
let drawn = false
|
|
78
82
|
|
|
83
|
+
const cursor = () => selectable[pos]
|
|
84
|
+
|
|
79
85
|
const draw = () => {
|
|
80
86
|
let out = drawn ? `\x1b[${totalLines}A` : ''
|
|
81
87
|
const hint = c.dim('(↑↓ move · space toggle · a all · enter confirm)')
|
|
82
88
|
out += `\x1b[2K${c.cyan('?')} ${c.bold(message)} ${hint}\n`
|
|
83
89
|
choices.forEach((choice, i) => {
|
|
84
|
-
|
|
90
|
+
if (choice.header) {
|
|
91
|
+
out += `\x1b[2K ${c.bold(choice.label)}\n`
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const active = i === cursor()
|
|
85
95
|
const box = selected.has(i) ? c.green('◉') : '◯'
|
|
86
96
|
const pointer = active ? c.cyan('❯') : ' '
|
|
87
|
-
out += `\x1b[2K${pointer} ${box} ${active ? c.cyan(choice.label) : choice.label}\n`
|
|
97
|
+
out += `\x1b[2K ${pointer} ${box} ${active ? c.cyan(choice.label) : choice.label}\n`
|
|
88
98
|
})
|
|
89
99
|
process.stdout.write(out)
|
|
90
100
|
drawn = true
|
|
@@ -93,17 +103,18 @@ function multiselect(message, choices) {
|
|
|
93
103
|
const onKey = (str, key) => {
|
|
94
104
|
if (!key) return
|
|
95
105
|
if (key.name === 'up' || key.name === 'k') {
|
|
96
|
-
|
|
106
|
+
pos = (pos - 1 + selectable.length) % selectable.length
|
|
97
107
|
draw()
|
|
98
108
|
} else if (key.name === 'down' || key.name === 'j') {
|
|
99
|
-
|
|
109
|
+
pos = (pos + 1) % selectable.length
|
|
100
110
|
draw()
|
|
101
111
|
} else if (key.name === 'space' || str === ' ') {
|
|
102
|
-
|
|
112
|
+
const i = cursor()
|
|
113
|
+
selected.has(i) ? selected.delete(i) : selected.add(i)
|
|
103
114
|
draw()
|
|
104
115
|
} else if (key.name === 'a') {
|
|
105
|
-
if (selected.size ===
|
|
106
|
-
else
|
|
116
|
+
if (selected.size === selectable.length) selected.clear()
|
|
117
|
+
else selectable.forEach((i) => selected.add(i))
|
|
107
118
|
draw()
|
|
108
119
|
} else if (key.name === 'return') {
|
|
109
120
|
teardown(onKey)
|
package/src/skills.js
CHANGED
|
@@ -7,6 +7,19 @@ const path = require('node:path')
|
|
|
7
7
|
// location so `cdragon` finds them no matter where it's invoked.
|
|
8
8
|
const SKILLS_DIR = path.resolve(__dirname, '..', 'skills')
|
|
9
9
|
|
|
10
|
+
// skills-lock.json (managed by the `skills` CLI) is the manifest of skills
|
|
11
|
+
// pulled from external sources. Anything not listed there is authored locally.
|
|
12
|
+
const LOCK_PATH = path.resolve(SKILLS_DIR, '..', 'skills-lock.json')
|
|
13
|
+
|
|
14
|
+
function installedSkillNames() {
|
|
15
|
+
try {
|
|
16
|
+
const lock = JSON.parse(fs.readFileSync(LOCK_PATH, 'utf8'))
|
|
17
|
+
return new Set(Object.keys(lock.skills || {}))
|
|
18
|
+
} catch {
|
|
19
|
+
return new Set()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
// Parse the YAML frontmatter of a SKILL.md into a flat key/value map.
|
|
11
24
|
// Handles plain `key: value` and block scalars (`>`, `|`) used for long
|
|
12
25
|
// descriptions, collapsing them into a single line.
|
|
@@ -55,6 +68,7 @@ function discoverSkills(dir) {
|
|
|
55
68
|
return []
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
const installed = installedSkillNames()
|
|
58
72
|
const skills = []
|
|
59
73
|
for (const entry of entries) {
|
|
60
74
|
if (!entry.isDirectory()) continue
|
|
@@ -66,6 +80,7 @@ function discoverSkills(dir) {
|
|
|
66
80
|
name: entry.name,
|
|
67
81
|
dir: path.join(dir, entry.name),
|
|
68
82
|
description: fm.description || '',
|
|
83
|
+
source: installed.has(entry.name) ? 'installed' : 'mine',
|
|
69
84
|
})
|
|
70
85
|
}
|
|
71
86
|
|