kward 0.74.0 → 0.76.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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +4 -2
  5. data/Rakefile +18 -0
  6. data/doc/configuration.md +75 -10
  7. data/doc/extensibility.md +6 -19
  8. data/doc/getting-started.md +1 -1
  9. data/doc/lifecycle-hooks.md +424 -0
  10. data/doc/memory.md +1 -1
  11. data/doc/personas.md +2 -2
  12. data/doc/plugins.md +2 -1
  13. data/doc/releasing.md +7 -15
  14. data/doc/rpc.md +111 -16
  15. data/doc/session-management.md +9 -9
  16. data/doc/skills.md +199 -0
  17. data/doc/usage.md +7 -7
  18. data/lib/kward/agent.rb +77 -22
  19. data/lib/kward/auth/anthropic_oauth.rb +5 -87
  20. data/lib/kward/auth/oauth_helpers.rb +105 -0
  21. data/lib/kward/auth/openai_oauth.rb +4 -86
  22. data/lib/kward/cli/commands.rb +9 -2
  23. data/lib/kward/cli/compaction.rb +7 -0
  24. data/lib/kward/cli/git.rb +30 -3
  25. data/lib/kward/cli/hook_commands.rb +258 -0
  26. data/lib/kward/cli/interactive_turn.rb +3 -0
  27. data/lib/kward/cli/memory_commands.rb +1 -5
  28. data/lib/kward/cli/plugins.rb +41 -1
  29. data/lib/kward/cli/prompt_interface.rb +28 -7
  30. data/lib/kward/cli/rendering.rb +8 -4
  31. data/lib/kward/cli/runtime_helpers.rb +44 -5
  32. data/lib/kward/cli/sessions.rb +13 -0
  33. data/lib/kward/cli/settings.rb +226 -152
  34. data/lib/kward/cli/slash_commands.rb +81 -42
  35. data/lib/kward/cli/tabs.rb +26 -6
  36. data/lib/kward/cli.rb +26 -3
  37. data/lib/kward/cli_transcript_formatter.rb +21 -18
  38. data/lib/kward/compaction/token_estimator.rb +112 -0
  39. data/lib/kward/compactor.rb +42 -116
  40. data/lib/kward/config_files.rb +120 -25
  41. data/lib/kward/deep_copy.rb +42 -0
  42. data/lib/kward/diff_view_mode.rb +36 -0
  43. data/lib/kward/events.rb +2 -0
  44. data/lib/kward/export_path.rb +2 -6
  45. data/lib/kward/frontmatter.rb +43 -0
  46. data/lib/kward/hooks/audit_log.rb +121 -0
  47. data/lib/kward/hooks/catalog.rb +110 -0
  48. data/lib/kward/hooks/command_handler.rb +66 -0
  49. data/lib/kward/hooks/config_loader.rb +104 -0
  50. data/lib/kward/hooks/decision.rb +102 -0
  51. data/lib/kward/hooks/event.rb +60 -0
  52. data/lib/kward/hooks/http_handler.rb +72 -0
  53. data/lib/kward/hooks/manager.rb +158 -0
  54. data/lib/kward/hooks/matcher.rb +75 -0
  55. data/lib/kward/hooks.rb +16 -0
  56. data/lib/kward/markdown_transcript.rb +4 -3
  57. data/lib/kward/mcp/client.rb +1 -1
  58. data/lib/kward/mcp/server_config.rb +7 -3
  59. data/lib/kward/mcp/stdio_transport.rb +7 -1
  60. data/lib/kward/memory/turn_context.rb +19 -0
  61. data/lib/kward/message_access.rb +2 -2
  62. data/lib/kward/message_text.rb +19 -0
  63. data/lib/kward/model/client.rb +23 -52
  64. data/lib/kward/model/context_usage.rb +10 -4
  65. data/lib/kward/model/copilot_models.rb +44 -0
  66. data/lib/kward/model/model_info.rb +38 -5
  67. data/lib/kward/model/payloads.rb +15 -17
  68. data/lib/kward/model/stream_parser.rb +183 -18
  69. data/lib/kward/pan/index.html.erb +1 -0
  70. data/lib/kward/pan/server.rb +53 -2
  71. data/lib/kward/path_guard.rb +13 -0
  72. data/lib/kward/plugin_registry.rb +88 -25
  73. data/lib/kward/prompt_interface/editor/controller.rb +60 -1
  74. data/lib/kward/prompt_interface/editor/renderer.rb +11 -0
  75. data/lib/kward/prompt_interface/editor/state.rb +5 -1
  76. data/lib/kward/prompt_interface/selection_prompt.rb +26 -6
  77. data/lib/kward/prompt_interface/transcript_renderer.rb +1 -1
  78. data/lib/kward/prompt_interface.rb +4 -1
  79. data/lib/kward/prompts/commands.rb +3 -3
  80. data/lib/kward/prompts.rb +7 -7
  81. data/lib/kward/rpc/mcp_status.rb +50 -0
  82. data/lib/kward/rpc/prompt_bridge.rb +38 -4
  83. data/lib/kward/rpc/runtime_payloads.rb +1 -1
  84. data/lib/kward/rpc/server.rb +95 -15
  85. data/lib/kward/rpc/session_manager.rb +260 -33
  86. data/lib/kward/rpc/session_metrics.rb +1 -1
  87. data/lib/kward/rpc/session_tree_rows.rb +13 -64
  88. data/lib/kward/rpc/tool_event_normalizer.rb +16 -2
  89. data/lib/kward/rpc/transcript_normalizer.rb +19 -20
  90. data/lib/kward/rpc/turn_context.rb +109 -0
  91. data/lib/kward/session_store.rb +6 -20
  92. data/lib/kward/session_tree_nodes.rb +98 -18
  93. data/lib/kward/session_tree_renderer.rb +6 -61
  94. data/lib/kward/skills/registry.rb +109 -17
  95. data/lib/kward/tools/mcp_tool.rb +4 -0
  96. data/lib/kward/tools/read_skill.rb +9 -4
  97. data/lib/kward/tools/registry.rb +266 -8
  98. data/lib/kward/tools/search/code.rb +4 -22
  99. data/lib/kward/tools/search/web.rb +28 -2
  100. data/lib/kward/update_check.rb +122 -0
  101. data/lib/kward/version.rb +1 -1
  102. data/lib/kward/workers/manager.rb +43 -3
  103. data/lib/kward/workspace.rb +3 -1
  104. data/templates/default/fulldoc/html/css/kward.css +22 -0
  105. data/templates/default/fulldoc/html/js/kward.js +2 -0
  106. data/templates/default/kward_navigation.rb +4 -2
  107. data/templates/default/layout/html/setup.rb +2 -0
  108. metadata +25 -1
@@ -11,6 +11,7 @@ require_relative "../telemetry/logger"
11
11
  require_relative "../telemetry/stats"
12
12
  require_relative "auth_manager"
13
13
  require_relative "config_manager"
14
+ require_relative "mcp_status"
14
15
  require_relative "redactor"
15
16
  require_relative "session_manager"
16
17
  require_relative "transport"
@@ -19,7 +20,7 @@ require_relative "transport"
19
20
  module Kward
20
21
  # JSON-RPC backend namespace used by UI clients.
21
22
  module RPC
22
- # Experimental JSON-RPC backend for UI clients.
23
+ # JSON-RPC backend for trusted local UI clients.
23
24
  #
24
25
  # The server speaks LSP-style Content-Length framing over stdin/stdout,
25
26
  # exposes capabilities during `initialize`, redacts secrets in errors and
@@ -47,14 +48,15 @@ module Kward
47
48
  PROTOCOL_METHODS = ["initialize", "shutdown"].freeze
48
49
  WORKSPACE_METHODS = ["workspace/validate", "workspace/info"].freeze
49
50
  TOOL_METHODS = ["tools/list"].freeze
51
+ MCP_METHODS = ["mcp/status"].freeze
50
52
  PROMPT_METHODS = ["prompts/list", "prompts/expand"].freeze
51
53
  SESSION_METHODS = [
52
54
  "sessions/create", "sessions/resume", "sessions/list", "sessions/rename",
53
55
  "sessions/clone", "sessions/compact", "sessions/forkMessages", "sessions/fork",
54
56
  "sessions/tree", "sessions/tree/setLabel", "sessions/tree/navigate",
55
- "sessions/export", "sessions/delete", "sessions/close", "sessions/transcript"
57
+ "sessions/export", "sessions/delete", "sessions/close", "sessions/transcript", "sessions/active"
56
58
  ].freeze
57
- TURN_METHODS = ["turns/start", "turns/cancel", "turns/status", "turns/events"].freeze
59
+ TURN_METHODS = ["turns/start", "turns/cancel", "turns/status", "turns/events", "turns/list", "turns/listActive"].freeze
58
60
  MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set"].freeze
59
61
  RUNTIME_METHODS = ["runtime/state", "runtime/stats"].freeze
60
62
  RUNTIME_SETTING_METHODS = ["runtime/updateSetting", "runtime/reload"].freeze
@@ -73,16 +75,21 @@ module Kward
73
75
  STARTUP_RESOURCE_METHODS = ["resources/startup"].freeze
74
76
  CONFIG_METHODS = ["config/read", "config/update"].freeze
75
77
  LOGGING_METHODS = ["logging/stats", "logging/tokenCsv"].freeze
78
+ LIFECYCLE_HOOK_METHODS = ["hooks/logs"].freeze
76
79
  UI_METHODS = ["ui/answerQuestion"].freeze
80
+ TOOL_APPROVAL_METHODS = ["tool/answerApproval"].freeze
77
81
  SESSION_EVENT_NOTIFICATION = "session/event"
78
82
  SESSION_UPDATED_NOTIFICATION = "session/updated"
79
83
  TURN_EVENT_NOTIFICATION = "turn/event"
80
84
  UI_QUESTION_NOTIFICATION = "ui/question"
81
85
  UI_FOOTER_NOTIFICATION = "ui/footer"
86
+ TOOL_APPROVAL_NOTIFICATION = "tool/approvalRequested"
87
+ HOOK_EVENT_NOTIFICATION = "hook/event"
82
88
  METHOD_GROUPS = {
83
89
  protocol: PROTOCOL_METHODS,
84
90
  workspace: WORKSPACE_METHODS,
85
91
  tools: TOOL_METHODS,
92
+ mcp: MCP_METHODS,
86
93
  prompts: PROMPT_METHODS,
87
94
  sessions: SESSION_METHODS,
88
95
  turns: TURN_METHODS,
@@ -96,7 +103,9 @@ module Kward
96
103
  startup_resources: STARTUP_RESOURCE_METHODS,
97
104
  config: CONFIG_METHODS,
98
105
  logging: LOGGING_METHODS,
99
- ui: UI_METHODS
106
+ lifecycle_hooks: LIFECYCLE_HOOK_METHODS,
107
+ ui: UI_METHODS,
108
+ tool_approval: TOOL_APPROVAL_METHODS
100
109
  }.freeze
101
110
  RPC_METHODS = METHOD_GROUPS.values.flatten.freeze
102
111
 
@@ -201,7 +210,9 @@ module Kward
201
210
  when WORKSPACE_METHODS[1]
202
211
  workspace_info(params["workspaceRoot"] || Dir.pwd)
203
212
  when TOOL_METHODS[0]
204
- { tools: ToolRegistry.new(workspace: configured_workspace).schemas }
213
+ @session_manager.tool_schemas(session_id: params["sessionId"])
214
+ when MCP_METHODS[0]
215
+ MCPStatus.new(config_manager: @config_manager).to_h
205
216
  when PROMPT_METHODS[0]
206
217
  prompts_list
207
218
  when PROMPT_METHODS[1]
@@ -236,6 +247,8 @@ module Kward
236
247
  logging_stats(params)
237
248
  when LOGGING_METHODS[1]
238
249
  logging_token_csv(params)
250
+ when LIFECYCLE_HOOK_METHODS[0]
251
+ lifecycle_hook_logs(params)
239
252
  when MEMORY_METHODS[0]
240
253
  @session_manager.memory_status
241
254
  when MEMORY_METHODS[1]
@@ -316,12 +329,16 @@ module Kward
316
329
  @session_manager.close_session(session_id: params.fetch("sessionId"))
317
330
  when SESSION_METHODS[14]
318
331
  @session_manager.transcript(session_id: params.fetch("sessionId"))
332
+ when SESSION_METHODS[15]
333
+ @session_manager.active_sessions
319
334
  when TURN_METHODS[0]
320
335
  @session_manager.start_turn(
321
336
  session_id: params.fetch("sessionId"),
322
337
  input: params.fetch("input"),
323
338
  streaming_behavior: params["streamingBehavior"],
324
- attachments: params["attachments"] || []
339
+ attachments: params["attachments"] || [],
340
+ options: params["options"] || {},
341
+ context: params["context"]
325
342
  )
326
343
  when TURN_METHODS[1]
327
344
  @session_manager.cancel_turn(turn_id: params.fetch("turnId"))
@@ -329,8 +346,14 @@ module Kward
329
346
  @session_manager.turn_status(turn_id: params.fetch("turnId"))
330
347
  when TURN_METHODS[3]
331
348
  @session_manager.turn_events(turn_id: params.fetch("turnId"), after_sequence: params["afterSequence"] || 0)
349
+ when TURN_METHODS[4]
350
+ @session_manager.list_turns(session_id: params["sessionId"])
351
+ when TURN_METHODS[5]
352
+ @session_manager.list_turns(session_id: params["sessionId"], active: true)
332
353
  when UI_METHODS[0]
333
354
  @session_manager.answer_question(session_id: params.fetch("sessionId"), question_request_id: params.fetch("questionRequestId"), answers: params.fetch("answers"))
355
+ when TOOL_APPROVAL_METHODS[0]
356
+ @session_manager.answer_tool_approval(session_id: params.fetch("sessionId"), approval_request_id: params.fetch("approvalRequestId"), approved: params.fetch("approved"))
334
357
  else
335
358
  raise NoMethodError, method
336
359
  end
@@ -340,7 +363,7 @@ module Kward
340
363
  {
341
364
  protocolVersion: PROTOCOL_VERSION,
342
365
  serverName: "kward",
343
- experimental: true,
366
+ experimental: false,
344
367
  capabilities: capabilities
345
368
  }
346
369
  end
@@ -349,7 +372,7 @@ module Kward
349
372
  {
350
373
  framing: "content-length",
351
374
  transcript: {
352
- format: "tauren-transcript-v1",
375
+ format: "kward-transcript-v1",
353
376
  messagesNormalized: true,
354
377
  supportsImages: true,
355
378
  supportsToolCalls: true,
@@ -363,10 +386,11 @@ module Kward
363
386
  methods: SESSION_METHODS,
364
387
  startupResume: { supported: true, method: SESSION_METHODS[0], parameter: "resumeLast", default: session_auto_resume_enabled?, immediateTranscript: true, sessionActivePersonaLabel: true },
365
388
  list: { supported: true, source: "rpc", ancestry: true, treeFields: true },
389
+ active: { supported: true, method: SESSION_METHODS[15] },
366
390
  fork: { supported: true, methods: SESSION_METHODS.values_at(6, 7), entryIdFormat: "entry-id", selectedMessage: "excludedFromForkComposerTextReturned" },
367
391
  compact: { supported: true, method: SESSION_METHODS[5], notification: SESSION_EVENT_NOTIFICATION, events: ["compactionStart", "compactionEnd"] },
368
392
  import: { supported: false },
369
- tree: { supported: true, method: SESSION_METHODS[8], labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "tauren-tree-items-v1" },
393
+ tree: { supported: true, method: SESSION_METHODS[8], labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "kward-tree-items-v1" },
370
394
  updates: { supported: false, notification: SESSION_UPDATED_NOTIFICATION }
371
395
  },
372
396
  turns: {
@@ -383,15 +407,29 @@ module Kward
383
407
  queuedTurns: "cancel-before-run",
384
408
  runningTurns: "stop-emitting-events-when-possible"
385
409
  },
386
- eventReplay: { behavior: "recent-in-memory", persisted: false, limit: SessionManager::RECENT_EVENT_LIMIT }
410
+ eventReplay: { behavior: "recent-in-memory", persisted: false, limit: SessionManager::RECENT_EVENT_LIMIT },
411
+ options: {
412
+ supported: true,
413
+ method: TURN_METHODS[0],
414
+ fields: ["provider", "model", "reasoningEffort", "allowedTools", "disabledTools", "approvalMode"],
415
+ perTurnModel: true,
416
+ perTurnReasoning: true,
417
+ perTurnToolScope: true
418
+ },
419
+ list: { supported: true, method: TURN_METHODS[4], activeMethod: TURN_METHODS[5] },
420
+ context: {
421
+ supported: true,
422
+ method: TURN_METHODS[0],
423
+ fields: ["activeFile", "openFiles", "selection", "diagnostics"]
424
+ }
387
425
  },
388
426
  events: {
389
427
  notification: TURN_EVENT_NOTIFICATION,
390
428
  assistantText: "assistantDelta",
391
- reasoning: { start: false, delta: true, end: false },
429
+ reasoning: { start: false, delta: true, boundary: true, end: false },
392
430
  modelRetry: { supported: true, event: "modelRetry" },
393
431
  steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
394
- tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
432
+ tools: { call: true, update: true, result: true, normalizedMetadata: true, diffs: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
395
433
  errors: true,
396
434
  sessionUpdates: false
397
435
  },
@@ -417,6 +455,20 @@ module Kward
417
455
  state: { supported: true },
418
456
  stats: { messageCounts: true, tokens: false, cost: false, contextUsage: true, contextUsageEstimated: true }
419
457
  },
458
+ lifecycleHooks: {
459
+ supported: true,
460
+ events: Hooks::Catalog.event_names,
461
+ decisions: Hooks::Decision::VALID_DECISIONS,
462
+ approvals: { supported: true, via: "toolApproval", requestNotification: TOOL_APPROVAL_NOTIFICATION, answerMethod: TOOL_APPROVAL_METHODS.first },
463
+ notifications: [HOOK_EVENT_NOTIFICATION, "hook/message"],
464
+ methods: LIFECYCLE_HOOK_METHODS,
465
+ auditLog: { supported: true, path: File.join(ConfigFiles.config_dir, "logs", "hooks.jsonl") },
466
+ commandHooks: true,
467
+ pluginHooks: true,
468
+ workspaceHooks: { supported: true, trustRequired: true, trustCommand: "/hooks trust" },
469
+ httpHooks: { supported: true, protocol: "jsonPost" },
470
+ asyncHooks: { supported: true, semantics: "observeOnly" }
471
+ },
420
472
  runtimeSettings: {
421
473
  supported: true,
422
474
  methods: RUNTIME_SETTING_METHODS,
@@ -424,7 +476,7 @@ module Kward
424
476
  },
425
477
  auth: {
426
478
  supported: true,
427
- providerFormat: "tauren-auth-v1",
479
+ providerFormat: "kward-auth-v1",
428
480
  methods: AUTH_METHODS,
429
481
  oauthProviders: ["openai", "anthropic", "github"],
430
482
  unsupportedOAuthProviders: { github: "CLI-only GitHub login for Copilot scaffolding; RPC login is not implemented yet." },
@@ -433,8 +485,21 @@ module Kward
433
485
  },
434
486
  memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
435
487
  workers: workers_capability,
488
+ stability: { protocol: "stable", compatibility: "additive-fields-unless-protocol-version-changes", experimentalCapabilities: ["workers"] },
436
489
  commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
437
- mcp: { supported: true, transport: "stdio", config: "mcpServers", exposes: ["tools"], unsupported: ["resources", "prompts", "sampling", "streamableHttp"] },
490
+ mcp: {
491
+ supported: true,
492
+ transport: "stdio",
493
+ config: "mcpServers",
494
+ exposes: ["tools"],
495
+ discovery: {
496
+ supported: true,
497
+ methods: TOOL_METHODS + MCP_METHODS,
498
+ toolMetadata: true,
499
+ serverStatus: true
500
+ },
501
+ unsupported: ["resources", "prompts", "sampling", "streamableHttp"]
502
+ },
438
503
  startupResources: { supported: true, method: STARTUP_RESOURCE_METHODS.first },
439
504
  starterPack: { supported: false, reason: "cliOnlyInstallCommand" },
440
505
  shell: { supported: false, reason: "interactiveTuiOnly" },
@@ -456,7 +521,7 @@ module Kward
456
521
  },
457
522
  security: {
458
523
  workspaceMutationGuard: "none",
459
- toolApproval: "none",
524
+ toolApproval: { supported: true, defaultMode: "none", modes: ["none", "ask"], requestNotification: TOOL_APPROVAL_NOTIFICATION, answerMethod: TOOL_APPROVAL_METHODS.first },
460
525
  canRunShell: true,
461
526
  canWriteFiles: true
462
527
  },
@@ -564,6 +629,21 @@ module Kward
564
629
  raise ArgumentError, e.message == TelemetryStats::USAGE ? e.message : "#{e.message} #{TelemetryStats::USAGE}"
565
630
  end
566
631
 
632
+ def lifecycle_hook_logs(params)
633
+ limit = params.fetch("limit", 20).to_i
634
+ limit = 20 unless limit.positive?
635
+ limit = 200 if limit > 200
636
+ path = File.join(ConfigFiles.config_dir, "logs", "hooks.jsonl")
637
+ return { path: path, records: [] } unless File.file?(path)
638
+
639
+ records = File.readlines(path, chomp: true).last(limit).filter_map do |line|
640
+ JSON.parse(line)
641
+ rescue JSON::ParserError
642
+ nil
643
+ end
644
+ { path: path, records: records }
645
+ end
646
+
567
647
  def commands_list(params)
568
648
  @session_manager.runtime_state(session_id: params.fetch("sessionId"))
569
649
  prompts = ConfigFiles.prompt_templates(reserved_commands: BUILTIN_SLASH_COMMAND_NAMES).map do |template|