library: Claude Desktop's skills list shows again, each skill's updatedAt an ISO time (mingxitang, #863) — magpie wrote a skill's updatedAt in the kind lastUpdated had, milliseconds where it is a number, but Desktop's own entries are ISO times or null and listLocalSkills checks the whole list at once: one number and the skills page stayed loading (failed to pass validation). Now updatedAt is always an ISO time and lastUpdated keeps its own kind; an entry written as milliseconds before is written as the same moment in ISO at the next sync, its other keys as they were. TestClaudeDesktopSkills: the entry of a manifest with millisecond lastUpdated, and an old numeric entry fixed at a sync; it fails on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (five timing tests under load 20 passed when run again).
This commit is contained in:
1 parent
69a7744a9f
commit
50d6c9a394
2 files changed
+66
-12
No files matched your search
@@ -106,17 +106,45 @@ func desktopEdited(p string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// desktopStamp is now, as the manifest's other times are written: a
|
||||
// number of milliseconds where they are numbers, else an ISO time.
|
||||
// desktopStamp is now as the manifest's lastUpdated is written: a number
|
||||
// of milliseconds where it is one, else an ISO time.
|
||||
func desktopStamp(raw []byte) any {
|
||||
t := gjson.GetBytes(raw, "lastUpdated")
|
||||
if !t.Exists() {
|
||||
t = gjson.GetBytes(raw, "skills.0.updatedAt")
|
||||
}
|
||||
if t.Type == gjson.Number {
|
||||
if gjson.GetBytes(raw, "lastUpdated").Type == gjson.Number {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
return time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
|
||||
return isoStamp(time.Now())
|
||||
}
|
||||
|
||||
// isoStamp is t as a skill's updatedAt is written. Desktop checks the whole
|
||||
// list of skills at once and takes each updatedAt to be such a string (or
|
||||
// null), whatever lastUpdated is: one number in it and none of the skills
|
||||
// are listed (#863).
|
||||
func isoStamp(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") }
|
||||
|
||||
// fixUpdatedAt is the entry with its updatedAt, when a number of
|
||||
// milliseconds (as magpie wrote them before #863), written as Desktop's
|
||||
// are, the other keys as they were; "" when it isn't a number.
|
||||
func fixUpdatedAt(e gjson.Result) string {
|
||||
if e.Get("updatedAt").Type != gjson.Number {
|
||||
return ""
|
||||
}
|
||||
v, _ := json.Marshal(isoStamp(time.UnixMilli(e.Get("updatedAt").Int())))
|
||||
var b strings.Builder
|
||||
b.WriteByte('{')
|
||||
e.ForEach(func(k, x gjson.Result) bool {
|
||||
if b.Len() > 1 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString(k.Raw + ":")
|
||||
if k.String() == "updatedAt" {
|
||||
b.Write(v)
|
||||
} else {
|
||||
b.WriteString(x.Raw)
|
||||
}
|
||||
return true
|
||||
})
|
||||
b.WriteByte('}')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// manifestEntry is a manifest's entry for the skill with that id, "" for
|
||||
@@ -140,7 +168,7 @@ func writeManifest(root string, put map[string]bool, drop []string) error {
|
||||
if err != nil && isFile(path) {
|
||||
return err
|
||||
}
|
||||
stamp := desktopStamp(raw)
|
||||
stamp, updated := desktopStamp(raw), isoStamp(time.Now())
|
||||
var list []json.RawMessage
|
||||
changed := false
|
||||
done := map[string]bool{}
|
||||
@@ -156,7 +184,7 @@ func writeManifest(root string, put map[string]bool, drop []string) error {
|
||||
// in the order Desktop writes them, any other key of the old
|
||||
// entry after them
|
||||
keys := []string{"skillId", "name", "description", "creatorType", "syncManaged", "updatedAt", "enabled"}
|
||||
vals := []any{name, name, desc, "user", false, stamp, enabled}
|
||||
vals := []any{name, name, desc, "user", false, updated, enabled}
|
||||
var b strings.Builder
|
||||
b.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
@@ -183,6 +211,9 @@ func writeManifest(root string, put map[string]bool, drop []string) error {
|
||||
case put[id] && !done[id]:
|
||||
list = append(list, entry(id, e.Raw))
|
||||
done[id], changed = true, true
|
||||
case fixUpdatedAt(e) != "":
|
||||
list = append(list, json.RawMessage(fixUpdatedAt(e)))
|
||||
changed = true
|
||||
default:
|
||||
list = append(list, json.RawMessage(e.Raw))
|
||||
}
|
||||
|
||||
@@ -130,8 +130,13 @@ func TestClaudeDesktopSkills(t *testing.T) {
|
||||
if len(es) != 3 || es["frontend-design"].CreatorType != "anthropic" || es["frontend-design"].Enabled || es["test"].Description != "Test" {
|
||||
t.Errorf("Desktop's own entries changed: %+v", es)
|
||||
}
|
||||
if _, ok := desktopEntries(t, b)["pdf"].UpdatedAt.(float64); !ok {
|
||||
t.Errorf("a manifest of millisecond times got another kind: %+v", desktopEntries(t, b)["pdf"])
|
||||
// #863: a skill's updatedAt is an ISO time even where lastUpdated is
|
||||
// milliseconds, which stays so
|
||||
if u, ok := desktopEntries(t, b)["pdf"].UpdatedAt.(string); !ok || !strings.HasSuffix(u, "Z") {
|
||||
t.Errorf("updatedAt isn't an ISO time: %+v", desktopEntries(t, b)["pdf"])
|
||||
}
|
||||
if !strings.Contains(read(t, filepath.Join(b, desktopManifest)), `"lastUpdated": 17`) {
|
||||
t.Errorf("lastUpdated's milliseconds changed kind:\n%s", read(t, filepath.Join(b, desktopManifest)))
|
||||
}
|
||||
// the other keys of a manifest, and its layout, stay
|
||||
if s := read(t, filepath.Join(a, desktopManifest)); !strings.HasPrefix(s, "{\n \"lastUpdated\"") {
|
||||
@@ -206,4 +211,22 @@ func TestClaudeDesktopSkills(t *testing.T) {
|
||||
if _, err := os.Stat(filepath.Join(b, "skills", "pdf", "SKILL.md")); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// #863: an updatedAt magpie wrote as milliseconds before is written as
|
||||
// an ISO time at the next sync, the same moment, the entry's other keys
|
||||
// and Desktop's own entries as they were
|
||||
write(t, m, strings.Replace(read(t, m), `"skills": [`, `"skills": [{"skillId":"ponytail","name":"ponytail","syncManaged":true,"updatedAt":1759536000000,"enabled":true},`, 1))
|
||||
ok(t)(Sync())
|
||||
es = desktopEntries(t, b)
|
||||
if es["ponytail"].UpdatedAt != "2025-10-04T00:00:00.000Z" || !es["ponytail"].SyncManaged || !es["ponytail"].Enabled {
|
||||
t.Errorf("the old entry: %+v\n%s", es["ponytail"], read(t, m))
|
||||
}
|
||||
for id, e := range es {
|
||||
if _, num := e.UpdatedAt.(float64); num {
|
||||
t.Errorf("%s: updatedAt is still a number", id)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(read(t, m), `{"skillId":"ponytail","name":"ponytail","syncManaged":true,"updatedAt":"2025-10-04T00:00:00.000Z","enabled":true}`) {
|
||||
t.Errorf("the entry's keys moved:\n%s", read(t, m))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user